-
- {
- shortcuts && this.renderShortcut(shortcuts)
- }
-
- {
- this.renderHeader(currentView, leftDate, 'left')
- }
-
-
-
-
-
- {
- this.renderHeader(currentView, rightDate, 'right')
- }
-
-
-
-
-
- {
- showTime && (
-
- {this.getRangeDateStr()}
-
- )
- }
- {
- showMask && (
-
{ this.setState({showMask: false}) }} />
- )
- }
- {
- showMask && (
- {
- this.setState({
- leftDate: d.startDate,
- rightDate: d.endDate
- })
- this.props.onPick(d, r)
- }}
- />
- )
- }
-
- )
- }
-}
-
-export default Provider(DatePanel)
diff --git a/components/date-picker/datepicker-legacy/Modal.js b/components/date-picker/datepicker-legacy/Modal.js
deleted file mode 100644
index 07aea567a..000000000
--- a/components/date-picker/datepicker-legacy/Modal.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import {Component} from 'react'
-import ReactDOM from 'react-dom'
-import clickOutSide from 'react-click-outside'
-class Modal extends Component {
- constructor (props) {
- super(props)
- this.el = document.createElement('div')
- this._body = document.body
- }
-
- componentDidMount () {
- this._body.appendChild(this.el)
- }
- handleClickOutside (e) {
- this.props.clickOutSide(e)
- }
- componentWillUnmount () {
- this._body.removeChild(this.el)
- }
- render () {
- return ReactDOM.createPortal(
- this.props.children,
- this.el
- )
- }
-}
-export default clickOutSide(Modal)
diff --git a/components/date-picker/datepicker-legacy/Time.js b/components/date-picker/datepicker-legacy/Time.js
deleted file mode 100644
index d74d35ec6..000000000
--- a/components/date-picker/datepicker-legacy/Time.js
+++ /dev/null
@@ -1,275 +0,0 @@
-import React, {Component} from 'react'
-import {deconstructDate} from './util'
-import Icon from '../../icon'
-import classNames from 'classnames'
-import { isSameDay } from './dateUtil'
-export default class TimePanel extends Component {
- constructor (props) {
- super(props)
- this.state = {
- date: new Date(props.date),
- prefix: {
- hours: 0,
- minutes: 0,
- seconds: 0
- },
- hoursArrow: false,
- minutesArrow: false,
- secondsArrow: false
- }
- this.hoursList = null
- this.minutesList = null
- this.secondsList = null
- this.liPrefix = props.onlyTime ? [1, 2, 3] : [1, 2, 3, 4]
- this.liSuffix = props.onlyTime ? [1, 2, 3] : [1, 2, 3, 4]
- this.hoursScrollEvent = this.scrollEvent.bind(this, 'hours')
- this.minutesScrollEvent = this.scrollEvent.bind(this, 'minutes')
- this.secondsScrollEvent = this.scrollEvent.bind(this, 'seconds')
- this.pageUpRef = React.createRef()
- this.pageDownRef = React.createRef()
- }
- range (num) {
- let arr = []
- for (let i = 0; i < num; i++) {
- arr.push(i < 10 ? '0' + i : i.toString())
- }
- return arr
- }
- componentDidMount () {
- this.completeScrollTop()
- }
- addListener () {
- this.hoursList && this.hoursList.addEventListener('scroll', this.hoursScrollEvent)
- this.minutesList && this.minutesList.addEventListener('scroll', this.minutesScrollEvent)
- this.secondsList && this.secondsList.addEventListener('scroll', this.secondsScrollEvent)
- }
- scrollEvent (type, e) {
- const st = e.target.scrollTop
- const val = Math.round(st / 32)
- const {date} = this.state
- if (type === 'hours') {
- date.setHours(val)
- } else if (type === 'minutes') {
- date.setMinutes(val)
- } else {
- date.setSeconds(val)
- }
- this.setState({
- date
- })
- this.props.onPick(date, true)
- }
- componentWillUnmount () {
- this.hoursList.removeEventListener('scroll', this.hoursScrollEvent)
- this.minutesList.removeEventListener('scroll', this.minutesScrollEvent)
- this.secondsList.removeEventListener('scroll', this.secondsScrollEvent)
- }
- componentWillReceiveProps (props) {
- if (!isSameDay(props.date, this.state.date)) {
- this.setState({
- date: props.date
- })
- }
- }
- completeScrollTop () {
- const {date} = this.state
- let {hours, minutes, seconds} = deconstructDate(date)
- const dVal = 32
- setTimeout(() => {
- this.hoursList.scrollTop = (hours) * dVal
- this.minutesList.scrollTop = (minutes) * dVal
- this.secondsList.scrollTop = (seconds) * dVal
- }, 0)
- setTimeout(() => {
- this.addListener()
- }, 200)
- }
-
- clickEvent (flag, e) {
- const li = e.target
- if (!li.innerText) return
- const {date} = this.state
- if (li.nodeName !== 'LI') return false
- const c = parseInt(li.innerText)
- if (flag === 'hours') {
- date.setHours(c)
- } else if (flag === 'minutes') {
- date.setMinutes(c)
- } else {
- date.setSeconds(c)
- }
- this.setState({
- date
- }, () => {
- this.completeScrollTop()
- })
- this.props.onPick(date, true)
- }
- _renderPrefix (val) {
- return val === 1 ?
: [
,
]
- }
- _renderSuffix (val, limit) {
- if (limit - val === 1) {
- return [
,
]
- } else if (limit - val === 2) {
- return
- }
- }
- scrollPageUp (type) {
- const {date} = this.state
- let cHours = date.getHours()
- let cMinutes = date.getMinutes()
- let cSeconds = date.getSeconds()
- if (type === 'hours') {
- cHours = cHours === 0 ? 0 : cHours - 1
- this.hoursList.scrollTop = cHours * 32
- }
- if (type === 'minutes') {
- cMinutes = cMinutes === 0 ? 0 : cMinutes - 1
- this.minutesList.scrollTop = cMinutes * 32
- }
- if (type === 'seconds') {
- cSeconds = cSeconds === 0 ? 0 : cSeconds - 1
- this.secondsList.scrollTop = cSeconds * 32
- }
- date.setHours(cHours, cMinutes, cSeconds)
- this.setState({date})
- }
- scrollPageDown (type) {
- const {date} = this.state
- let cHours = date.getHours()
- let cMinutes = date.getMinutes()
- let cSeconds = date.getSeconds()
- if (type === 'hours') {
- cHours = cHours === 23 ? 23 : cHours + 1
- this.hoursList.scrollTop = cHours * 32
- }
- if (type === 'minutes') {
- cMinutes = cMinutes === 59 ? 59 : cMinutes + 1
- this.minutesList.scrollTop = cMinutes * 32
- }
- if (type === 'seconds') {
- cSeconds = cSeconds === 59 ? 59 : cSeconds + 1
- this.secondsList.scrollTop = cSeconds * 32
- }
- date.setHours(cHours, cMinutes, cSeconds)
- this.setState({date}, () => {
- this.props.onPick(date, true)
- })
- }
- renderArrow (type) {
- return (
-
- this.scrollPageUp(type)}>
-
-
- this.scrollPageDown(type)}>
-
-
-
- )
- }
- render () {
- const { localeDatas } = this.props
- const { hours: lHours, minutes: lMinutes, seconds: lSeconds } = localeDatas.datePicker
- const {date, hoursArrow, minutesArrow, secondsArrow} = this.state
- const {hours, minutes, seconds} = deconstructDate(date)
- this.liPrefix = this.liPrefix.map((item, index) => {
- return
- })
- this.liSuffix = this.liSuffix.map((item, index) => {
- return
- })
- return (
-
-
- {lHours}
- {lMinutes}
- {lSeconds}
-
-
-
this.setState({hoursArrow: true})}
- onMouseLeave={() => this.setState({hoursArrow: false})}
- >
-
{ this.hoursList = el }}
- className='hi-timepicker__list'
- onClick={this.clickEvent.bind(this, 'hours')}
- >
- {this.liPrefix}
- {
- this.range(24).map((m, n) => {
- const _class = classNames(
- 'hi-timepicker__item',
- n === hours && 'hi-timepicker__item--current'
- )
- return (
- - {m}
- )
- })
- }
- {this.liSuffix}
-
- {hoursArrow && this.renderArrow('hours')}
-
-
this.setState({minutesArrow: true})}
- onMouseLeave={() => this.setState({minutesArrow: false})}
- >
-
{ this.minutesList = el }}
- onClick={this.clickEvent.bind(this, 'minutes')}
- >
- {this.liPrefix}
- {
- this.range(60).map((m, n) => {
- return - {m}
- })
- }
- {this.liSuffix}
-
- {minutesArrow && this.renderArrow('minutes')}
-
-
this.setState({secondsArrow: true})}
- onMouseLeave={() => this.setState({secondsArrow: false})}
- >
-
{ this.secondsList = el }}
- className='hi-timepicker__list'
- onClick={this.clickEvent.bind(this, 'seconds')}
- >
- {this.liPrefix}
- {
- this.range(60).map((m, n) => {
- return - {m}
- })
- }
- {this.liSuffix}
-
- {secondsArrow && this.renderArrow('seconds')}
-
-
- {/* {hours}
- {minutes}
- {seconds} */}
-
- {/*
-
-
-
-
- */}
-
-
- )
- }
-}
diff --git a/components/date-picker/datepicker-legacy/TimePanel.js b/components/date-picker/datepicker-legacy/TimePanel.js
deleted file mode 100644
index ed212311e..000000000
--- a/components/date-picker/datepicker-legacy/TimePanel.js
+++ /dev/null
@@ -1,35 +0,0 @@
-import React, {Component} from 'react'
-import Time from './Time'
-import Provider from '../../context'
-
-class TimePanel extends Component {
- constructor (props) {
- super(props)
- this.state = {
- style: props.style
- }
- if (this.props.type !== 'time') {
- this.state.style = {
- position: 'relative'
- }
- }
- }
- onTimePick (date, bol) {
- const {showTime} = this.props
- if (showTime) {
- this.props.onPick(date, true)
- } else {
- this.props.onPick(date, bol)
- }
- }
- render () {
- const { localeDatas, date, type } = this.props
- return (
-
-
-
- )
- }
-}
-
-export default Provider(TimePanel)
diff --git a/components/date-picker/datepicker-legacy/TimePeriodPanel.js b/components/date-picker/datepicker-legacy/TimePeriodPanel.js
deleted file mode 100644
index b506a8167..000000000
--- a/components/date-picker/datepicker-legacy/TimePeriodPanel.js
+++ /dev/null
@@ -1,66 +0,0 @@
-import React, {Component} from 'react'
-import classNames from 'classnames'
-import Provider from '../../context'
-import {getHours, getMinutes} from './dateUtil'
-class TimePeriodPanel extends Component {
- constructor (props) {
- super(props)
- this.listEl = React.createRef()
- }
- calcInterval () {
- const { timeInterval } = this.props
- const segment = 24 * 60 / timeInterval
- let pre = 0
- let next = 0
- let arr = []
- const func = (val) => (val < 10 ? '0' + val : val)
- for (let i = 0; i < segment; i++) {
- next += timeInterval
- const time = func(parseInt(pre / 60)) + ':' + func(pre % 60) + ' ~ ' + func(parseInt(next / 60)) + ':' + func(next % 60)
- arr.push(time)
- pre = next
- }
- return arr
- }
- getActiveIndex () {
- const { date, timeInterval } = this.props
- return timeInterval >= 60 ? getHours(date) * 60 / timeInterval : (getMinutes(date) + getHours(date) * 60) / timeInterval
- }
- componentDidMount () {
- setTimeout(() => {
- this.listEl.current.scrollTop = this.getActiveIndex() * 37
- }, 0)
- }
- render () {
- const list = this.calcInterval()
- const { onTimePeriodPick, localeDatas } = this.props
- const activeIndex = this.getActiveIndex()
- return
-
{localeDatas.datePicker.timePeriod}
-
-
- {
- list.map((item, index) => {
- const cls = classNames(
- 'hi-datepicker-legacy__period-item',
- activeIndex === index && 'hi-datepicker-legacy__period-item--active'
- )
- return - {
- const ts = item.split(' ~ ')
- onTimePeriodPick(ts[0], ts[1])
- }}
- >{item}
- })
- }
-
-
-
- }
-}
-
-export default Provider(TimePeriodPanel)
diff --git a/components/date-picker/datepicker-legacy/TimePicker.js b/components/date-picker/datepicker-legacy/TimePicker.js
deleted file mode 100644
index cc7dcd508..000000000
--- a/components/date-picker/datepicker-legacy/TimePicker.js
+++ /dev/null
@@ -1,40 +0,0 @@
-import React from 'react'
-import PropTypes from 'prop-types'
-
-import DatePickerType from './Type'
-import BasePicker from './BasePicker'
-import TimePanel from './TimePanel'
-import Provider from '../../context'
-import TimeRangePanel from './TimeRangePanel'
-class TimePicker extends BasePicker {
- initPanel (state, props) {
- return (
- props.type === 'time'
- ?
- :
- )
- }
-}
-TimePicker.propTypes = {
- type: PropTypes.oneOf(Object.values(DatePickerType)),
- date: PropTypes.instanceOf(Date),
- size: PropTypes.string,
- onChange: PropTypes.func,
- format: PropTypes.string
-}
-TimePicker.defaultProps = {
- type: 'time',
- format: 'HH:mm:ss',
- disabled: false
-}
-export default Provider(TimePicker)
diff --git a/components/date-picker/datepicker-legacy/TimeRangePanel.js b/components/date-picker/datepicker-legacy/TimeRangePanel.js
deleted file mode 100644
index c3bb0b018..000000000
--- a/components/date-picker/datepicker-legacy/TimeRangePanel.js
+++ /dev/null
@@ -1,36 +0,0 @@
-import React, {Component} from 'react'
-import Time from './Time'
-import Provider from '../../context'
-
-class TimeRangePanel extends Component {
- constructor (props) {
- super(props)
- this.state = {
- date: props.date,
- style: props.style
- }
- }
- onTimePick (flag, _date, bol) {
- const {date} = this.props
- const sd = flag === 'left' ? _date : date.startDate
- const ed = flag === 'right' ? _date : date.endDate
- let r = {
- startDate: sd,
- endDate: ed
- }
- this.props.onPick(r, bol)
- }
- render () {
- const { localeDatas, date, style } = this.props
- const {startDate, endDate} = date
- return (
-
- )
- }
-}
-
-export default Provider(TimeRangePanel)
diff --git a/components/date-picker/datepicker-legacy/Type.js b/components/date-picker/datepicker-legacy/Type.js
deleted file mode 100644
index cb923071d..000000000
--- a/components/date-picker/datepicker-legacy/Type.js
+++ /dev/null
@@ -1,25 +0,0 @@
-export default {
- // 默认普通日期(年月日)选择器
- Date: 'date',
- // 日期时间
- DateTime: 'datetime',
- // 时间
- Time: 'time',
- // 周
- Week: 'week',
- // 月
- Month: 'month',
- // 年
- Year: 'year',
- // 日期范围选择
- DateRange: 'daterange',
- // 周范围选择
- WeekRange: 'weekrange',
- // 时间范围选择
- TimeRange: 'timerange',
- // 时间段选择
- TimePeriod: 'timeperiod',
- // 日期时间范围选择
- DateTimeRange: 'datetimerange'
-
-}
diff --git a/components/date-picker/datepicker-legacy/WeekRangePanel.js b/components/date-picker/datepicker-legacy/WeekRangePanel.js
deleted file mode 100644
index d041862f9..000000000
--- a/components/date-picker/datepicker-legacy/WeekRangePanel.js
+++ /dev/null
@@ -1,237 +0,0 @@
-import React, {Component} from 'react'
-import {deconstructDate, nextMonth} from './util'
-import Calender from './Calender'
-import Icon from '../../icon'
-import classNames from 'classnames'
-import {startOfWeek, endOfWeek, isSameMonth} from './dateUtil'
-export default class WeekRangePanel extends Component {
- constructor (props) {
- super(props)
- const {startDate, endDate} = props.date
- let leftDate = new Date(startDate)
- let rightDate = endDate || nextMonth(leftDate)
- if (endDate) {
- if (isSameMonth(startDate, endDate)) {
- rightDate = nextMonth(leftDate)
- }
- }
- this.state = {
- date: leftDate,
- range: {
- startDate: startOfWeek(startDate),
- endDate: endDate ? endOfWeek(endDate) : endOfWeek(startDate),
- selecting: false
- },
- leftDate,
- rightDate
- }
- }
- /**
- * Header 中间部分内容
- * @param {String} type 选择器类型
- * @param {Number} year 当前年份
- * @param {Number} month 当前月份
- */
- getHeaderCenterContent (year, month) {
- const { localeDatas, locale } = this.props
- const {currentView} = this.state
- if (currentView === 'year') {
- return (year - 4) + '~' + (year + 7)
- }
- let arr = [localeDatas.datePicker.monthShort[month - 1]]
- if (locale === 'zh-CN') {
- arr.unshift(year + '年 ')
- } else {
- arr.push(` ${year}`)
- }
- return arr
- }
- /**
- * 渲染 Header 部分(包含箭头快捷操作)
- * @param {Object} args {
- * year: 当前年份
- * month: 当前月份
- * type: 选择器类型
- * num: 点击箭头时要步进的值 默认1 主要应用于年份选择时
- * }
- */
- renderHeader (type, value, lr) {
- const {year, month} = deconstructDate(value)
- return (
-
-
- this.changeYear(true, lr)} >
- this.changeMonth(true, lr)} >
-
-
- {this.getHeaderCenterContent(year, month)}
-
- {/*
- {month}月
- */}
-
- this.changeMonth(false, lr)} >
- this.changeYear(false, lr)} >
-
-
- )
- }
- /**
- * 改变月份事件
- * @param {Number} num 加减值
- */
- changeMonth (flag, pos) {
- let {leftDate, rightDate} = this.state
- let nLeftDate = new Date(leftDate.getTime())
- let nRightDate = new Date(rightDate.getTime())
- let left = deconstructDate(nLeftDate)
- let right = deconstructDate(nRightDate)
- if (pos === 'left') {
- if (flag) {
- left.month -= 1
- if (left.month < 0) {
- left.month = 12
- left.year -= 1
- }
- } else {
- left.month += 1
- if (left.month > 12) {
- left.month = 1
- left.year += 1
- }
- }
- nLeftDate.setFullYear(left.year)
- nLeftDate.setMonth(left.month - 1)
- } else {
- if (flag) {
- right.month -= 1
- if (right.month < 0) {
- right.month = 12
- right.year -= 1
- }
- } else {
- right.month += 1
- if (right.month > 12) {
- right.month = 1
- right.year += 1
- }
- }
- if (left.month === right.month - 1) {
- this.setState({
- disableArrow: {
- month: false
- }
- })
- }
- nRightDate.setFullYear(right.year)
- nRightDate.setMonth(right.month - 1)
- }
- if (nLeftDate <= nRightDate) {
- this.setState({
- leftDate: nLeftDate,
- rightDate: nRightDate
- })
- }
- }
- /**
- * 改变年份事件
- * @param {Number} num 加减值
- */
- changeYear (flag, pos) {
- let {leftDate, rightDate} = this.state
- let nLeftDate = new Date(leftDate.getTime())
- let nRightDate = new Date(rightDate.getTime())
- let left = deconstructDate(nLeftDate)
- let right = deconstructDate(nRightDate)
- if (pos === 'left') {
- if (flag) {
- left.year -= 1
- } else {
- left.year += 1
- }
- nLeftDate.setFullYear(left.year)
- // this.setState({
- // leftDate
- // })
- } else {
- if (flag) {
- right.year -= 1
- } else {
- right.year += 1
- }
- nRightDate.setFullYear(right.year)
- // this.setState({
- // rightDate
- // })
- }
- if (nLeftDate <= nRightDate) {
- this.setState({
- leftDate: nLeftDate,
- rightDate: nRightDate
- })
- }
- }
- pick (startDate, endDate) {
- const {range} = this.state
- const {onPick} = this.props
- range.startDate = startDate
- range.endDate = endDate
- this.setState({
- range
- })
- if (endDate) {
- onPick(range)
- }
- }
- onMouseMoveHandler (date) {
- const {range} = this.state
- range.endDate = date
- this.setState({
- range
- })
- }
- render () {
- const {range, leftDate, rightDate} = this.state
- const {type, theme} = this.props
- const _c = classNames(
- 'hi-datepicker-legacy',
- theme && 'theme__' + theme
- )
- // const {year, month, day} = deconstructDate(date)
- // const _date = new Date(year, month, day)
-
- return (
-
-
-
- {this.renderHeader(type, leftDate, 'left')}
-
-
-
-
-
- {this.renderHeader(type, rightDate, 'right')}
-
-
-
-
-
-
- )
- }
-}
diff --git a/components/date-picker/datepicker-legacy/constants.js b/components/date-picker/datepicker-legacy/constants.js
deleted file mode 100644
index b1cadcbe1..000000000
--- a/components/date-picker/datepicker-legacy/constants.js
+++ /dev/null
@@ -1,52 +0,0 @@
-import {getYearWeek} from './util'
-import dateFormat from 'date-fns/format'
-export const TIME_BLOCK = {
- lineHeight: 22,
- padding: 8
-}
-export const DAY_MILLISECONDS = 86400000
-export const RANGE_SPLIT = '~'
-export const FORMATS = {
- date: 'yyyy-MM-dd',
- month: 'yyyy-MM',
- year: 'yyyy',
- time: 'HH:mm:ss',
- daterange: 'yyyy-MM-dd',
- week: 'yyyy',
- weekrange: 'yyyy',
- timeperiod: 'yyyy-MM-dd HH:mm:ss'
-}
-
-export const isVaildDate = (date) => {
- return date && (date instanceof Date || date.startDate || typeof date === 'number')
-}
-export const formatterDate = (type, date, formatter, showTime, localeDatas, weekOffset = 0) => {
- if (!isVaildDate(date)) {
- return ''
- }
- date = new Date(date)
- let str = ''
- switch (type) {
- // case 'daterange':
- // if (date instanceof Date) { date = {startDate: date, endDate: date} }
- // str = dateFormat(date.startDate, `${formatter}${showTime ? ' hh:mm:ss' : ''}`) + RANGE_SPLIT + dateFormat(date.endDate, `${formatter}${showTime ? ' hh:mm:ss' : ''}`)
- // break
- case 'weekrange':
- // if (date instanceof Date) { date = {startDate: date, endDate: date} }
- str = localeDatas.datePicker.weekrange(date.getFullYear(), getYearWeek(date, weekOffset).weekNum)
- break
- case 'year':
- case 'month':
- case 'time':
- str = dateFormat(date, formatter)
- break
- case 'week':
- str = localeDatas.datePicker.weekrange(date.getFullYear(), getYearWeek(date, weekOffset).weekNum)
- break
- default:
- str = dateFormat(date, `${formatter}${showTime ? ' HH:mm:ss' : ''}`)
- break
- }
-
- return str
-}
diff --git a/components/date-picker/datepicker-legacy/dateUtil.js b/components/date-picker/datepicker-legacy/dateUtil.js
deleted file mode 100644
index 115f552c0..000000000
--- a/components/date-picker/datepicker-legacy/dateUtil.js
+++ /dev/null
@@ -1,77 +0,0 @@
-import getDaysInMonth from 'date-fns/getDaysInMonth'
-import subMonths from 'date-fns/subMonths'
-import getDay from 'date-fns/getDay'
-import startOfMonth from 'date-fns/startOfMonth'
-import isWithinInterval from 'date-fns/isWithinInterval'
-import isSameDay from 'date-fns/isSameDay'
-import compareAsc from 'date-fns/compareAsc'
-import startOfDay from 'date-fns/startOfDay'
-import endOfDay from 'date-fns/endOfDay'
-import parse from 'date-fns/parse'
-import toDate from 'date-fns/toDate'
-import startOfWeek from 'date-fns/startOfWeek'
-import endOfWeek from 'date-fns/endOfWeek'
-import dateFormat from 'date-fns/format'
-import addMonths from 'date-fns/addMonths'
-import isSameMonth from 'date-fns/isSameMonth'
-import getYear from 'date-fns/getYear'
-import getMonth from 'date-fns/getMonth'
-import isToday from 'date-fns/isToday'
-import getHours from 'date-fns/getHours'
-import getMinutes from 'date-fns/getMinutes'
-import getSeconds from 'date-fns/getSeconds'
-import addHours from 'date-fns/addHours'
-import subDays from 'date-fns/subDays'
-import differenceInDays from 'date-fns/differenceInDays'
-import dfIsValid from 'date-fns/isValid'
-import addYears from 'date-fns/addYears'
-import subYears from 'date-fns/subYears'
-import parseISO from 'date-fns/parseISO'
-
-const isValid = (date) => {
- return (date && dfIsValid(toDate(date)))
-}
-const getValidDate = (date) => {
- return isValid(date) ? toDate(date) : new Date()
-}
-const getStartDate = (dateObj) => {
- return getValidDate(dateObj.startDate)
-}
-const getEndDate = (dateObj) => {
- return getValidDate(dateObj.endDate)
-}
-
-export {
- getDaysInMonth, // 获取当月的天数
- subMonths, // 月份减法
- addMonths, // 月份加法
- addYears, // 年份加
- subYears, // 年份减
- getDay, // 获取周几
- startOfMonth, // 指定日期月份的1日
- isWithinInterval, // 是否在指定日期范围内
- isSameDay, // 是否是同一天(忽略时分秒)
- compareAsc, // 比较两个日期
- startOfDay, // 一天的开始
- endOfDay, // 一天的结束
- parse, // 解析成日期
- startOfWeek, // 一周的开始
- endOfWeek, // 一周的结束
- dateFormat, // 格式化时间
- isSameMonth, // 是否是同一个月
- getYear, // 获取年
- getMonth, // 获取月
- isToday, // 是否是今天
- getHours, // 获取小时
- getMinutes, // 获取分钟
- getSeconds, // 获取秒
- addHours, // 小时加
- subDays, // 天减
- differenceInDays, // 相差多少天
- isValid, // 是否有效时间
- getStartDate, // 封装 - 获取开始时间
- getEndDate, // 封装 - 获取结果时间
- getValidDate, // 获取有效的时间
- toDate,
- parseISO
-}
diff --git a/components/date-picker/datepicker-legacy/index.js b/components/date-picker/datepicker-legacy/index.js
deleted file mode 100644
index 7eb52a122..000000000
--- a/components/date-picker/datepicker-legacy/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import DatePicker from './DatePicker'
-import './style/index'
-export default DatePicker
diff --git a/components/date-picker/datepicker-legacy/local.js b/components/date-picker/datepicker-legacy/local.js
deleted file mode 100644
index 5377c6b8f..000000000
--- a/components/date-picker/datepicker-legacy/local.js
+++ /dev/null
@@ -1,4 +0,0 @@
-export default {
- month: [ '一', '二', '三', '四', '五', '六', '七', '八', '九', '十', '十一', '十二' ],
- week: [ '日', '一', '二', '三', '四', '五', '六' ]
-}
diff --git a/components/date-picker/datepicker-legacy/style/index.js b/components/date-picker/datepicker-legacy/style/index.js
deleted file mode 100644
index 34ed1fc38..000000000
--- a/components/date-picker/datepicker-legacy/style/index.js
+++ /dev/null
@@ -1,2 +0,0 @@
-import './index.scss'
-// import './timepicker.scss'
diff --git a/components/date-picker/datepicker-legacy/style/index.scss b/components/date-picker/datepicker-legacy/style/index.scss
deleted file mode 100644
index 004479440..000000000
--- a/components/date-picker/datepicker-legacy/style/index.scss
+++ /dev/null
@@ -1,612 +0,0 @@
-@import '@hi-ui/core-css/index.scss';
-
-$basic-color: #4284f5 !default;
-
-.hi-datepicker-legacy {
- position: relative;
- // display: flex;
- background: #fff;
- z-index: 1060;
- font-size: 14px;
- box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.12);
-
- &__input-root {
- display: inline-block;
- }
-
- &__input {
- background: #fff;
- border: 1px solid #d8d8d8;
- border-radius: 2px;
- height: 32px;
- width: 180px;
- display: flex;
- align-items: center;
- justify-content: space-around;
- box-sizing: border-box;
-
- &:hover {
- border: 1px solid #4284f5;
- }
-
- &--range {
- width: 320px;
-
- input {
- flex: 0 1 40%;
- text-align: center;
- }
- }
-
- input {
- outline: none;
- border: 0;
- min-width: 0;
- min-height: 0;
- height: 28px;
- padding-left: 10px;
- flex: 1;
-
- &::-webkit-input-placeholder {
- color: #ccc;
- }
- }
-
- &--disabled {
- .hi-datepicker-legacy__input-icon,
- input {
- cursor: not-allowed;
- background: #f4f4f4;
- color: rgba(44, 48, 78, 0.2);
- }
- }
-
- &--middle {
- width: 240px;
- }
- }
-
- &__input-icon {
- height: 100%;
- flex: 0 0 32px;
- line-height: 32px;
- text-align: center;
- box-sizing: border-box;
- cursor: pointer;
- }
-
- &__header {
- border-bottom: 1px solid #f2f2f2;
- display: flex;
- justify-content: space-between;
- height: 47px;
- align-items: center;
- padding: 0 15px;
-
- &-btns {
- width: 50px;
- display: flex;
- justify-content: space-around;
-
- .hi-icon {
- cursor: pointer;
- flex: 1;
- }
- }
-
- &-text {
- font-weight: bold;
- cursor: pointer;
- }
- }
-
- &__body {
- background: #fff;
- border: 1px solid #d1dbe5;
- border-radius: 2px;
- box-sizing: border-box;
- width: 288px;
-
- &--hastime {
- width: 468px;
- display: flex;
-
- .hi-timepicker {
- box-shadow: none;
- border-left: 1px solid #f2f2f2;
- }
- }
-
- &--period {
- display: flex;
- width: 432px;
- }
-
- &--range {
- width: 578px;
- display: flex;
- border: none;
-
- .hi-datepicker-legacy__panel {
- flex: 1;
- }
- }
-
- &--shortcuts {
- width: 683px;
- }
- }
-
- &__time-period {
- width: 144px;
- border-left: 1px solid #f2f2f2;
- text-align: center;
- }
-
- &__period-header {
- line-height: 48px;
- height: 48px;
- font-weight: 600;
- border-bottom: 1px solid #f2f2f2;
- box-sizing: border-box;
- }
-
- &__period-list {
- list-style: none;
- margin: 0;
- padding: 0;
- margin-top: 8px;
- overflow: auto;
- height: 241px;
- }
-
- &__period-item {
- padding: 8px;
- cursor: pointer;
-
- &:hover {
- background: rgba(240, 246, 255, 1);
- }
-
- &--active {
- color: #4284f5;
- }
- }
-
- &__footer {
- height: 48px;
- border-top: 1px solid #f2f2f2;
- box-sizing: border-box;
- line-height: 48px;
- text-align: center;
- cursor: pointer;
- position: relative;
-
- &:hover {
- color: #4284f5;
- }
- }
-
- &__mask {
- width: 100%;
- height: 100%;
- background: rgba(0, 0, 0, 0.4);
- position: absolute;
- top: 0;
- left: 0;
- }
-
- &__time-text {
- position: absolute;
- width: 148px;
- height: 32px;
- border-radius: 2px;
- left: 50%;
- top: 8px;
- transform: translateX(-50%);
- background: #fff;
- z-index: 2;
- line-height: 32px;
- }
-
- &__calender-container {
- padding: 17px 18px 15px;
-
- &--year,
- &--month {
- padding: 10px 5px;
-
- .hi-datepicker-legacy__row {
- height: 57px;
- }
-
- .hi-datepicker-legacy__cell {
- width: 92px;
-
- &.today {
- .hi-datepicker-legacy__text {
- background: $basic-color;
- opacity: 1;
- color: #fff;
- }
- }
- }
-
- .hi-datepicker-legacy__text {
- width: 48px;
- }
- }
-
- &--week {
- .hi-datepicker-legacy__row {
- &:hover {
- .hi-datepicker-legacy__content {
- background: rgba(66, 132, 245, 0.1);
- }
- }
- }
- }
- }
-
- &__calender {
- border: 0;
- width: 100%;
- border-collapse: separate;
- border-spacing: 0;
-
- th {
- line-height: 24px;
- padding: 0;
- }
- }
-
- &__row {
- &::after {
- content: '';
- }
-
- &--current-week {
- .range-se {
- .hi-datepicker-legacy__content {
- background: $basic-color;
- }
- }
- }
- }
-
- // &__content {
- // margin: 0 3px;
- // background: #fff;
- // color: #fff;
- // border-radius: 2px;
- // }
-
- &__cell {
- cursor: pointer;
- text-align: center;
- line-height: 24px;
- padding: 4px 0;
- width: 36px;
- height: 24px;
-
- &.disabled {
- .hi-datepicker-legacy__content {
- cursor: not-allowed;
- color: map-get(get-palette($basic-color), 'g80');
- background: rgba(246, 246, 246, 1);
- }
- }
-
- &.range-se:not(.prev):not(.next) {
- .hi-datepicker-legacy__content {
- margin: 0 3px;
- background: $basic-color;
- color: #fff;
- border-radius: 2px;
- }
- }
-
- &.prev,
- &.next {
- .hi-datepicker-legacy__text {
- color: rgba(44, 48, 78, 0.2);
- }
- }
-
- // &.in-range:not(.next):not(.prev) {
- // .hi-datepicker-legacy__content {
- // background: rgba(66, 132, 245, 0.1);
- // }
- // }
-
- &.today:not(.in-range) {
- .hi-datepicker-legacy__text {
- border: 1px solid $basic-color;
- opacity: 0.8;
- }
- }
-
- &.current:not(.in-range) {
- .hi-datepicker-legacy__text {
- background: $basic-color;
- color: #fff;
- }
- }
- }
-
- &__text {
- display: inline-block;
- width: 24px;
- height: 24px;
- box-sizing: border-box;
- border-radius: 2px;
- }
-
- &__shortcuts {
- background: #fff;
- border-right: 1px solid #f2f2f2;
- width: 105px;
- text-align: center;
-
- p {
- cursor: pointer;
- }
- }
-
- .hide {
- display: none !important;
- }
-}
-
-.hi-timepicker-legacy {
- width: 180px;
- background: #fff;
- box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.12);
- z-index: 1;
-
- .hi-timepicker__body {
- width: 180px;
- }
-
- &--timerange {
- width: 400px;
- display: flex;
- justify-content: space-between;
- }
-
- &__split {
- flex: 1;
- height: 248px;
- margin-top: 47px;
- align-self: flex-end;
- border: 1px solid #f2f2f2;
- border-bottom: none;
- }
-
- &__timeheader {
- display: flex;
- height: 48px;
- text-align: center;
- align-items: center;
- font-size: 14px;
- font-weight: 600;
- color: rgba(51, 51, 51, 1);
- border-bottom: 1px solid rgba(242, 242, 242, 1);
- box-sizing: border-box;
- }
-
- &__mark {
- flex: 1;
- }
-
- &__timebody {
- display: flex;
- height: 248px;
- // border-bottom: 1px solid #f2f2f2;
- position: relative;
- box-sizing: border-box;
- overflow: hidden;
- padding: 0;
-
- &::-webkit-scrollbar {
- display: none;
- }
- }
-
- &__page-turn {
- position: absolute;
- z-index: 2;
- width: 16px;
- height: 16px;
- line-height: 16px;
- text-align: center;
- font-size: 16px;
- left: 22px;
- top: 0;
- cursor: pointer;
-
- &:last-child {
- top: auto;
- bottom: 0;
- }
- }
-
- &__list-container {
- width: 60px;
- overflow: hidden;
- position: relative;
- padding: 12px 0;
- }
-
- &__list {
- // position: absolute;
- // z-index: 2;
- width: 80px;
- height: 224px;
- margin: 0;
- list-style-type: none;
- // overflow-x: hidden;
- overflow-y: auto;
- padding: 0;
-
- &:nth-child(2) {
- left: 60px;
- z-index: 3;
- }
-
- &:nth-child(3) {
- left: 120px;
- z-index: 4;
- }
- }
-
- &__item {
- text-align: center;
- font-size: 14px;
- color: rgba(0, 0, 0, 0.65);
- line-height: 32px;
- height: 32px;
- width: 60px;
- cursor: pointer;
- box-sizing: border-box;
- // border-right: 1px solid rgba(242, 242, 242, 1);
- background-color: #fff;
-
- &--empty {
- cursor: default;
- }
-
- &--current {
- font-weight: bold;
- }
-
- &--disabled {
- color: rgba(44, 48, 78, 0.2);
- cursor: not-allowed;
- }
-
- &:hover:not(.hi-timepicker__item--current):not(.hi-timepikcer__item--empty):not(.hi-timepicker__item--disabled) {
- background-color: #eff5fe;
- }
- }
-
- &__footer {
- padding: 2px 0;
- text-align: right;
-
- button {
- margin-right: 4px;
- }
- }
-
- &__current-line {
- height: 32px;
- box-sizing: border-box;
- border-top: 1px solid rgba(242, 242, 242, 1);
- border-bottom: 1px solid rgba(242, 242, 242, 1);
-
- // background: rgba(247, 247, 247, 1);
- position: absolute;
- top: 108px;
- width: 156px;
- z-index: 4;
- pointer-events: none;
- display: flex;
- text-align: center;
- align-items: center;
- margin: 0 12px;
-
- span {
- flex: 1;
- }
- }
-}
-
-@each $key, $value in $palette-primary {
- .theme__#{$key} {
- .hi-datepicker-legacy {
- &__time-header {
- &--active {
- color: map-get(get-palette($value), '50');
- }
- }
-
- &__time-footer {
- color: map-get(get-palette($value), '50');
- }
-
- &__calender-container--year,
- &__calender-container--month {
- .hi-datepicker-legacy__cell {
- &.today {
- .hi-datepicker-legacy__text {
- border: 1px solid map-get(get-palette($value), '50') !important;
- color: map-get(get-palette($value), '50') !important;
- background: #fff !important;
- }
- }
- }
- }
-
- &__row {
- &--current-week {
- .hi-datepicker-legacy__content {
- // TODO: 色板值
- // background: map-get(get-palette($value), '10');
- background: rgba(66, 132, 245, 0.1);
- }
-
- .range-se {
- .hi-datepicker-legacy__content {
- background: map-get(get-palette($value), '50');
- color: #fff;
- margin: 0 3px;
- border-radius: 2px;
- }
-
- .hi-datepicker-legacy__text {
- color: #fff;
- }
- }
- }
- }
-
- &__cell {
- &.today:not(.in-range) {
- .hi-datepicker-legacy__text {
- border: 1px solid map-get(get-palette($value), '50');
- color: map-get(get-palette($value), '50');
- }
- }
-
- &.in-range:not(.next):not(.prev) {
- .hi-datepicker-legacy__content {
- // TODO: 色板值
- // background: map-get(get-palette($value), '10');
- background: rgba(66, 142, 245, 0.1);
- }
- }
-
- &.range-se:not(.next):not(.prev) {
- .hi-datepicker-legacy__content {
- background: map-get(get-palette($value), '50');
- }
- }
-
- &:hover:not(.today):not(.current):not(.in-range):not(.range-se):not(.disabled) {
- .hi-datepicker-legacy__text {
- // TODO: 色板值
- background: rgba(66, 142, 245, 0.1);
- // background: map-get(get-palette($value), '10');
- }
- }
- }
-
- &__body {
- .hi-datepicker-legacy__time-footer {
- color: map-get(get-palette($value), '40');
- }
- }
- }
- }
-}
diff --git a/components/date-picker/datepicker-legacy/style/timepicker.scss b/components/date-picker/datepicker-legacy/style/timepicker.scss
deleted file mode 100644
index 82d0321ac..000000000
--- a/components/date-picker/datepicker-legacy/style/timepicker.scss
+++ /dev/null
@@ -1,40 +0,0 @@
-.hi-timepicker {
- width: 180px;
- background: #fff;
-
- &__timebody {
- display: flex;
- justify-content: space-around;
- height: 195px;
- border-bottom: 1px solid #f2f2f2;
- }
-
- &__time-list {
- overflow: auto;
- flex: 1;
-
- &-el {
- text-align: center;
- font-size: 14px;
- color: rgba(0, 0, 0, 0.65);
- line-height: 22px;
- padding: 8px;
- border-right: 1px solid #f2f2f2;
- cursor: pointer;
-
- &_current {
- background: #f7f7f7;
- font-weight: bold;
- }
- }
- }
-
- &__time-footer {
- padding: 2px 0;
- text-align: right;
-
- button {
- margin-right: 4px;
- }
- }
-}
diff --git a/components/date-picker/datepicker-legacy/util.js b/components/date-picker/datepicker-legacy/util.js
deleted file mode 100644
index 1cde52bd6..000000000
--- a/components/date-picker/datepicker-legacy/util.js
+++ /dev/null
@@ -1,38 +0,0 @@
-import { addMonths, getDay, subDays, differenceInDays, startOfWeek, endOfWeek } from './dateUtil'
-
-export const deconstructDate = (date, weekOffset = 0) => {
- !(date instanceof Date) && (date = new Date(date))
- return {
- year: date.getFullYear(),
- month: date.getMonth() + 1,
- week: getYearWeek(date, weekOffset).weekNum,
- day: date.getDate(),
- hours: date.getHours(),
- minutes: date.getMinutes(),
- seconds: date.getSeconds(),
- time: date.getTime()
- }
-}
-export const getYearWeek = (date, weekStart = 0) => {
- const year = date.getFullYear()
- let date1 = new Date(year, parseInt(date.getMonth()), date.getDate())
- let date2 = new Date(year, 0, 1)
- let num = getDay(date2)
- date2 = subDays(date2, weekStart ? (num - 1) : num) // 周日开始
- const din = differenceInDays(date1, date2)
- return {
- weekNum: Math.ceil((din + 1) / 7),
- start: startOfWeek(date1, {weekStartsOn: weekStart}),
- end: endOfWeek(date1, {weekStartsOn: weekStart})
- }
-}
-
-export const calcDayCount = (year, month) => {
- return new Date(year, parseInt(month, 10), 0).getDate()
-}
-
-export const nextMonth = function (date) {
- !(date instanceof Date) && (date = new Date(date))
-
- return addMonths(date, 1)
-}
diff --git a/components/date-picker/hooks/index.js b/components/date-picker/hooks/index.js
new file mode 100644
index 000000000..b7866b287
--- /dev/null
+++ b/components/date-picker/hooks/index.js
@@ -0,0 +1,6 @@
+import useDate from './useDate'
+import usePlaceholder from './usePlaceholder'
+import useCalenderData from './useCalenderData'
+import useFormat from './useFormat'
+import useAltData from './useAltData'
+export { useDate, usePlaceholder, useCalenderData, useFormat, useAltData }
diff --git a/components/date-picker/hooks/useAltData.js b/components/date-picker/hooks/useAltData.js
new file mode 100644
index 000000000..2a8f55da8
--- /dev/null
+++ b/components/date-picker/hooks/useAltData.js
@@ -0,0 +1,90 @@
+import React, { useState, useEffect } from 'react'
+import { getPRCDate, deconstructDate } from '../utils'
+
+const useAltData = ({
+ altCalendarPreset,
+ altCalendar,
+ dateMarkPreset,
+ dateMarkRender
+}) => {
+ const [altCalendarPresetData, setAltCalendarPresetData] = useState({})
+ const [dateMarkPresetData, setDateMarkPresetData] = useState({})
+
+ // 获取预置数据
+ const getLunarPresetData = () => {
+ const allPRCDate = {}
+ if (['zh-CN', 'id-ID'].includes(altCalendarPreset)) {
+ const _urlKey =
+ altCalendarPreset === 'zh-CN' ? 'PRCLunar' : 'IndiaHoliday'
+ getPRCDate(_urlKey).then(res => {
+ Object.keys(res.data).forEach(key => {
+ let oneYear = {}
+ res.data[key][_urlKey].forEach(item => {
+ Object.assign(oneYear, {
+ [item.date.replace(/-/g, '/')]: {
+ ...item,
+ highlight: altCalendarPreset === 'id-ID'
+ }
+ })
+ })
+ Object.assign(allPRCDate, oneYear)
+ })
+ setAltCalendarPresetData(
+ altCalendar ? getAltCalendarData(allPRCDate) : allPRCDate
+ )
+ })
+ } else {
+ setAltCalendarPresetData(
+ altCalendar ? getAltCalendarData(allPRCDate) : {}
+ )
+ }
+ }
+ // 获取预置数据
+ const getMarkPresetData = () => {
+ if (altCalendarPreset && altCalendarPreset !== 'zh-CN') {
+ return
+ }
+ if (dateMarkPreset === 'zh-CN') {
+ getPRCDate('PRCHoliday').then(res => {
+ const allPRCDate = {}
+ Object.keys(res.data).forEach(key => {
+ Object.keys(res.data[key].PRCHoliday).forEach(elkey => {
+ allPRCDate[elkey.replace(/-/g, '/')] =
+ res.data[key].PRCHoliday[elkey] === '1' ? (
+
+ 休
+
+ ) : (
+
+ 班
+
+ )
+ })
+ })
+ setDateMarkPresetData(allPRCDate)
+ })
+ }
+ }
+ // 合并用户自定义的日期信息作为presetData
+ const getAltCalendarData = allPRCDate => {
+ const allData = {}
+ altCalendar.length > 0 &&
+ altCalendar.forEach(item => {
+ const dateInfo = deconstructDate(item.date)
+ if (!Number.isNaN(dateInfo.year)) {
+ Object.assign(allData, {
+ [dateInfo.year + '/' + dateInfo.month + '/' + dateInfo.day]: item
+ })
+ }
+ })
+ return Object.assign(allPRCDate, allData)
+ }
+
+ useEffect(() => {
+ altCalendarPreset && getLunarPresetData()
+ dateMarkPreset && getMarkPresetData()
+ }, [])
+ return [altCalendarPresetData, dateMarkPresetData]
+}
+
+export default useAltData
diff --git a/components/date-picker/hooks/useCalenderData.js b/components/date-picker/hooks/useCalenderData.js
new file mode 100644
index 000000000..e6f4cb9d0
--- /dev/null
+++ b/components/date-picker/hooks/useCalenderData.js
@@ -0,0 +1,231 @@
+import { useState, useEffect } from 'react'
+import moment from 'moment'
+import { DAY_MILLISECONDS } from '../constants'
+import _ from 'lodash'
+
+const getYearOrMonthRows = ({
+ originDate,
+ renderDate,
+ type,
+ view,
+ range,
+ localeDatas
+}) => {
+ const _date = renderDate ? moment(renderDate) : moment()
+ const start = view === 'year' ? _date.year() - 4 : 0
+ let trs = [[], [], [], []]
+ let num = 0
+ const current = moment()
+ for (let i = 0; i < 4; i++) {
+ let row = trs[i]
+ for (let j = 0; j < 3; j++) {
+ let col = row[j] || (row[j] = { type: 'normal' })
+ const y = start + num
+ view === 'year'
+ ? (col.text = y)
+ : (col.text = localeDatas.datePicker.month[y])
+ col.value = y
+ num++
+ const currentYM = _date[view](y)
+ if (currentYM.isSame(current, view)) {
+ col.type = 'today'
+ }
+ if (type.includes('range') && type.includes(view)) {
+ if (
+ range.start &&
+ range.end &&
+ (currentYM.isBetween(range.start, range.end) ||
+ currentYM.isBetween(range.end, range.start))
+ ) {
+ col.range = true
+ }
+ if (range.start && currentYM.isSame(range.start, view)) {
+ col.type = 'selected'
+ col.range = false
+ }
+ if (range.end && currentYM.isSame(range.end, view)) {
+ col.type = 'selected'
+ col.range = false
+ }
+ continue
+ }
+ if (originDate && (y === originDate.year() || y === originDate.month())) {
+ col.type = 'selected'
+ }
+ }
+ }
+ return trs
+}
+const getTime = (week, y, m) => {
+ const r = new Date(y, m - 1, 1)
+ const t = r.getTime() - week * DAY_MILLISECONDS
+ return t
+}
+const getDateRows = ({
+ originDate,
+ range,
+ type,
+ weekOffset,
+ min,
+ max,
+ renderDate,
+ view
+}) => {
+ const rows = [[], [], [], [], [], []]
+ const today = moment()
+ let _date = moment(renderDate)
+ // * dayCount: 当月天数
+ // * lastMonthDayCount: 上月总天数
+ // * firstDayWeek: 当月第一天是周几
+ let firstDayWeek = _date.startOf('month').day() - weekOffset
+ if (firstDayWeek <= 0) {
+ // 如果为0 代表该月第一天是周日,在日历上需要第二行开始显示
+ firstDayWeek = 7
+ }
+ const startTimeByCurrentPanel = getTime(
+ firstDayWeek,
+ _date.year(),
+ _date.month() + 1
+ )
+ const dayCount = _date.daysInMonth()
+ const lastMonthDayCount = moment(_date)
+ .subtract(1, 'months')
+ .daysInMonth()
+ let count = 0
+ for (let i = 0; i < 6; i++) {
+ let row = rows[i]
+ for (let j = 0; j < 7; j++) {
+ let col =
+ row[j] ||
+ (row[j] = {
+ type: 'normal',
+ range: false,
+ rangeStart: false,
+ rangeEnd: false
+ })
+ const currentTime = moment(
+ startTimeByCurrentPanel + DAY_MILLISECONDS * (i * 7 + j)
+ )
+ let isPN = false // is Prev Or Next Month
+ const isDisabled =
+ currentTime.isBefore(moment(min)) || currentTime.isAfter(moment(max)) // isDisabled cell
+ if (i === 0) {
+ // 处理第一行的日期数据
+ if (j >= firstDayWeek) {
+ // 本月
+ col.value = ++count
+ } else {
+ // 上月
+ col.value = lastMonthDayCount - firstDayWeek + j + 1
+ col.type = 'prev'
+ isPN = true
+ }
+ } else {
+ ++count
+ if (count <= dayCount) {
+ // 本月
+ col.value = count
+ } else {
+ // 下月
+ col.value = count - dayCount
+ col.type = 'next'
+ isPN = true
+ }
+ }
+
+ if (isDisabled) {
+ col.type = 'disabled'
+ }
+ if (!isPN && currentTime.isSame(today, 'day')) {
+ col.type = 'today'
+ }
+ if (type.includes('range') && !isPN) {
+ if (
+ currentTime.isBetween(range.start, range.end) ||
+ currentTime.isBetween(range.end, range.start)
+ ) {
+ col.range = true
+ }
+ if (range.start && currentTime.isSame(range.start, 'day')) {
+ col.type = 'selected'
+ col.range = false
+ }
+ if (range.end && currentTime.isSame(range.end, 'day')) {
+ col.type = 'selected'
+ col.range = false
+ }
+ continue
+ }
+ if (originDate && currentTime.isSame(originDate, 'day') && !isPN) {
+ col.type = 'selected'
+ }
+
+ if (type === 'week') {
+ const weekNum = weekOffset ? currentTime.isoWeek() : currentTime.week()
+ row.weekNum = weekNum
+ if (originDate) {
+ const _d = _.cloneDeep(originDate)
+ const wFirst = moment(_d)
+ .startOf('week')
+ .add(weekOffset, 'days')
+ const wLast = moment(_d)
+ .endOf('week')
+ .add(weekOffset, 'days')
+ if (
+ currentTime.isSame(wFirst, 'day') ||
+ currentTime.isSame(wLast, 'day')
+ ) {
+ col.type = 'selected'
+ continue
+ }
+ if (currentTime.isBetween(wFirst, wLast)) {
+ col.type = 'normal'
+ col.range = true
+ }
+ }
+ }
+ }
+ }
+ return rows
+}
+const useDate = ({
+ view,
+ date,
+ originDate,
+ weekOffset,
+ localeDatas,
+ range,
+ type,
+ min,
+ max,
+ renderDate
+}) => {
+ const [rows, setRows] = useState([])
+ useEffect(() => {
+ const _rows =
+ view.includes('month') || view.includes('year')
+ ? getYearOrMonthRows({
+ originDate,
+ renderDate,
+ type,
+ view,
+ localeDatas,
+ range
+ })
+ : getDateRows({
+ originDate,
+ range,
+ type,
+ weekOffset,
+ min,
+ max,
+ renderDate,
+ view
+ })
+ setRows(_rows)
+ }, [renderDate, view, range])
+
+ return [rows]
+}
+
+export default useDate
diff --git a/components/date-picker/hooks/useDate.js b/components/date-picker/hooks/useDate.js
new file mode 100644
index 000000000..db7c61d5b
--- /dev/null
+++ b/components/date-picker/hooks/useDate.js
@@ -0,0 +1,27 @@
+import { useState, useEffect } from 'react'
+import moment from 'moment'
+import _ from 'lodash'
+const parseValue = (value, type) => {
+ if (!value) return [null]
+ const _value = moment(value)
+ const isValid = moment(value).isValid()
+ if (value && typeof value === 'object' && type.includes('range')) {
+ return [value.start ? moment(value.start) : null, value.end ? moment(value.end) : null]
+ }
+ return [isValid ? _value : null]
+}
+const useDate = ({ value, defaultValue, cacheDate, type }) => {
+ const [outDate, setOutDate] = useState([])
+ const changeOutDate = (dates) => {
+ setOutDate(_.cloneDeep(dates))
+ }
+ useEffect(() => {
+ const d = parseValue(value || defaultValue, type)
+ setOutDate(d)
+ cacheDate.current = d
+ }, [value])
+
+ return [outDate, changeOutDate]
+}
+
+export default useDate
diff --git a/components/date-picker/hooks/useFormat.js b/components/date-picker/hooks/useFormat.js
new file mode 100644
index 000000000..be83eb5b9
--- /dev/null
+++ b/components/date-picker/hooks/useFormat.js
@@ -0,0 +1,11 @@
+import { FORMATS } from '../constants'
+
+const useFormat = ({ type, showTime, format, locale = 'zh-CN' }) => {
+ let _format = format || FORMATS(locale)[type]
+ if (showTime && !/[H|h|m|s]/.test(_format)) {
+ _format += ' HH:mm:ss'
+ }
+ return [_format]
+}
+
+export default useFormat
diff --git a/components/date-picker/hooks/usePlaceholder.js b/components/date-picker/hooks/usePlaceholder.js
new file mode 100644
index 000000000..8da811761
--- /dev/null
+++ b/components/date-picker/hooks/usePlaceholder.js
@@ -0,0 +1,29 @@
+import { useState, useEffect } from 'react'
+
+const parsePlaceholder = ({ type, placeholder: _placeholder, showTime, localeDatas }) => {
+ const typePlaceholder = localeDatas.datePicker.placeholders[type]
+ const tempPlaceholder = showTime ? localeDatas.datePicker.placeholderTimeperiod : typePlaceholder || [localeDatas.datePicker.placeholder]
+
+ let leftPlaceholder = tempPlaceholder[0]
+ let rightPlaceholder = tempPlaceholder[1] || leftPlaceholder
+
+ if (_placeholder instanceof Array) {
+ leftPlaceholder = _placeholder[0]
+ rightPlaceholder = _placeholder[1] || _placeholder[0]
+ } else if (typeof placeholder === 'string') {
+ leftPlaceholder = _placeholder
+ rightPlaceholder = _placeholder
+ }
+ return [leftPlaceholder, rightPlaceholder]
+}
+const usePlaceholder = (args) => {
+ const [placeholders, setPlaceholders] = useState([])
+
+ useEffect(() => {
+ setPlaceholders(parsePlaceholder(args))
+ }, [])
+
+ return [placeholders]
+}
+
+export default usePlaceholder
diff --git a/components/date-picker/hooks/useTimePeriodData.js b/components/date-picker/hooks/useTimePeriodData.js
new file mode 100644
index 000000000..8c80afd9a
--- /dev/null
+++ b/components/date-picker/hooks/useTimePeriodData.js
@@ -0,0 +1,16 @@
+const useTimeperiodData = (timeInterval) => {
+ const segment = 24 * 60 / timeInterval
+ let pre = 0
+ let next = 0
+ let periodData = []
+ const func = (val) => (val < 10 ? '0' + val : val)
+ for (let i = 0; i < segment; i++) {
+ next += timeInterval
+ const time = func(parseInt(pre / 60)) + ':' + func(pre % 60) + ' ~ ' + func(parseInt(next / 60)) + ':' + func(next % 60)
+ periodData.push(time)
+ pre = next
+ }
+ return [periodData]
+}
+
+export default useTimeperiodData
diff --git a/components/date-picker/index.d.ts b/components/date-picker/index.d.ts
new file mode 100644
index 000000000..8288996ab
--- /dev/null
+++ b/components/date-picker/index.d.ts
@@ -0,0 +1,47 @@
+
+export type DateRange = {
+ start: Date | string | number | undefined | null
+ end: Date | string | number | undefined | null
+}
+type DateRangeString = {
+ start: string
+ end: string
+}
+type CalendarItem = {
+ date?: Date | string
+ text?: string
+ highlight?: boolean
+}
+export interface CommonProps {
+ value?: Date | string | number | DateRange | undefined | null
+ defaultValue?: Date | string | number | DateRange | undefined | null
+ disabled?: boolean
+ clearable?: boolean
+ placeholder?: string | string[]
+ format?: string
+ onChange?: (date: Date | DateRange, dateStr: string | DateRangeString) => void
+}
+type Shortcuts = {
+ title: string
+ range: Date[] | number[]
+}
+
+interface DateProps extends CommonProps {
+ type?: 'date' | 'daterange' | 'year' | 'month' | 'week' | 'weekrange' | 'timeperiod' | 'yearrange' | 'monthrange'
+ min?: Date
+ minDate?: Date
+ max?: Date
+ maxDate?: Date
+ disabledDate?: (currentDate: Date) => boolean
+ showTime?: boolean
+ shortcuts?: string[] | Shortcuts[]
+ weekOffset?: 0 | 1
+ altCalendar?: CalendarItem
+ altCalendarPreset?: 'zh-CN' | 'id-ID'
+ dateMarkRender?: (currentDate: Date, today: Date) => JSX.Element
+ dateMarkPreset?: 'zh-CN'
+ overlayClassName?: string
+}
+
+declare const DatePicker: React.ComponentType
+export default DatePicker
diff --git a/components/date-picker/index.js b/components/date-picker/index.js
deleted file mode 100644
index c2c60cdfc..000000000
--- a/components/date-picker/index.js
+++ /dev/null
@@ -1,7 +0,0 @@
-import DatePicker from './DatePicker'
-import { dateFormat } from './dateUtil'
-import './style/index'
-
-DatePicker.format = dateFormat
-
-export default DatePicker
diff --git a/components/date-picker/index.jsx b/components/date-picker/index.jsx
new file mode 100644
index 000000000..4088ba8c1
--- /dev/null
+++ b/components/date-picker/index.jsx
@@ -0,0 +1,8 @@
+import BasePicker from './BasePicker'
+import TimePicker from './TimePicker'
+import moment from 'moment'
+BasePicker.format = (date, format) => {
+ return moment(date).format(format)
+}
+export default BasePicker
+export { TimePicker }
diff --git a/components/date-picker/local.js b/components/date-picker/local.js
deleted file mode 100644
index 5377c6b8f..000000000
--- a/components/date-picker/local.js
+++ /dev/null
@@ -1,4 +0,0 @@
-export default {
- month: [ '一', '二', '三', '四', '五', '六', '七', '八', '九', '十', '十一', '十二' ],
- week: [ '日', '一', '二', '三', '四', '五', '六' ]
-}
diff --git a/components/date-picker/style/index.js b/components/date-picker/style/index.js
index 34ed1fc38..63810a681 100644
--- a/components/date-picker/style/index.js
+++ b/components/date-picker/style/index.js
@@ -1,2 +1 @@
import './index.scss'
-// import './timepicker.scss'
diff --git a/components/date-picker/style/index.scss b/components/date-picker/style/index.scss
index 9c80aed01..4aa85cead 100644
--- a/components/date-picker/style/index.scss
+++ b/components/date-picker/style/index.scss
@@ -1,154 +1,176 @@
-@import '@hi-ui/core-css/index.scss';
+@import '../../core-css/index.scss';
-$basic-color: #4284f5 !default;
-$error-color: #f44141 !default;
+$error-color: get-color($palette-secondary, 'danger') !default;
.hi-datepicker {
position: relative;
- // display: flex;
- background: #fff;
+ background: use-color('white');
z-index: 1060;
- font-size: 14px;
box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.12);
- .hi-datepicker__indiaHoli {
- display: none;
- position: absolute;
- top: 36px;
- left: 0;
- width: 430px;
- text-align: center;
+ &__popper {
+ width: 288px;
+ font-size: 14px;
+ background: use-color('white');
+ box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.15);
+ border-radius: 4px;
+ display: flex;
+ flex-wrap: wrap;
- &-text {
- display: inline-block;
- max-width: 410px;
- word-break: break-all;
- background: #edf2fc;
- border: 1px solid #d8e5ff;
- padding: 7px 12px;
- border-radius: 2px;
- }
+ &--range {
+ width: 576px;
- &-enter {
- display: none;
- opacity: 0;
- transition: all 0.3s ease-in;
+ &.hi-datepicker__popper--large {
+ width: 866px;
+ }
}
- &-enter-done {
- display: block;
- opacity: 1;
- transition: all 0.3s ease-in;
+ &--timeperiod,
+ &--large {
+ width: 432px;
}
- &-exit {
- display: block;
- opacity: 1;
+ &--time {
+ width: 468px;
}
- &-exit-done {
- display: none;
- opacity: 0;
- transition: all 0.3s ease-in;
+ &--shortcuts {
+ width: 681px;
}
}
- &__input-root {
- display: inline-block;
+ &__root {
+ display: inline-flex;
}
- &__input {
- background: #fff;
- border: 1px solid #d8d8d8;
+ &__picker {
+ background: use-color('white');
+ border: 1px solid use-color('gray-30');
border-radius: 2px;
height: 32px;
width: 180px;
- display: flex;
+ display: inline-flex;
align-items: center;
justify-content: space-around;
box-sizing: border-box;
- &--error {
- border: 1px solid $error-color;
- }
-
- &:hover:not(.hi-datepicker__input--disabled,.hi-datepicker__input--error) {
- border: 1px solid $basic-color;
+ &:hover:not(.hi-datepicker__picker--disabled):not(.hi-datepicker__picker--error) {
+ border: 1px solid use-color('primary');
}
&--focus {
- border: 1px solid $basic-color;
+ border: 1px solid use-color('primary');
}
- .icon-close-circle {
- color: #bbb;
+ &--error {
+ border: 1px solid $error-color;
}
- .icon-close-circle:hover {
- color: #333;
+ &--disabled {
+ background: rgba(242, 242, 242, 1);
}
- &--range {
+ &--daterange,
+ &--weekrange,
+ &--yearrange,
+ &--timerange,
+ &--monthrange {
width: 320px;
- &-time {
+ input {
+ text-align: center;
+ }
+
+ &.hi-datepicker__picker--hastime {
width: 400px;
}
+ }
+
+ &--timeperiod {
+ width: 400px;
input {
- flex: 0 1 40%;
text-align: center;
}
}
+ &--hastime {
+ width: 240px;
+ }
+
+ .hi-icon {
+ margin-top: 2px;
+ margin-right: 8px;
+ font-size: 14px;
+ cursor: pointer;
+
+ &--disabled {
+ cursor: not-allowed;
+ color: rgba(204, 204, 204, 1);
+ }
+ }
+
+ .icon-close-circle {
+ color: use-color('gray-50');
+ }
+
+ .icon-close-circle:hover {
+ color: use-color('black');
+ }
+
input {
outline: none;
border: 0;
- min-width: 0;
- min-height: 0;
height: 28px;
padding-left: 10px;
- flex: 1;
+ width: 100%;
&::-webkit-input-placeholder {
- color: #ccc;
+ color: use-color('gray-50');
}
- }
-
- &--connection {
- height: 30px;
- line-height: 32px;
- }
- &--disabled {
- .hi-datepicker__input-icon,
- input,
- span {
+ &.disabled {
cursor: not-allowed;
- background: #f4f4f4;
- color: rgba(44, 48, 78, 0.2);
+ background: rgba(242, 242, 242, 1);
}
}
+ }
- &--middle {
- width: 240px;
- }
+ &__input {
+ display: inline-flex;
+ justify-content: space-between;
+ align-items: center;
+ width: 100%;
}
- &__input-icon {
- height: 100%;
- flex: 0 0 32px;
- line-height: 32px;
- text-align: center;
- box-sizing: border-box;
- cursor: pointer;
+ &__panel {
+ // width: 288px;
+ width: 100%;
+ // height: 296px;
+ background: use-color('white');
+ box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.15);
+ display: flex;
+
+ .hi-datepicker__panel--left {
+ min-width: 288px;
+ flex: 1;
+ }
+
+ .hi-datepicker__panel--right {
+ flex: 1;
+ }
+
+ &--noshadow {
+ box-shadow: none;
+ }
}
&__header {
- border-bottom: 1px solid #f2f2f2;
+ border-bottom: 1px solid use-color('gray-30');
display: flex;
justify-content: space-between;
- height: 47px;
+ height: 48px;
+ box-sizing: border-box;
align-items: center;
padding: 0 12px;
@@ -160,12 +182,10 @@ $error-color: #f44141 !default;
.hi-icon {
cursor: pointer;
flex: 1;
- }
- .hi-icon:hover {
- cursor: pointer;
- flex: 1;
- color: #4284f5;
+ &:hover {
+ color: use-color('primary');
+ }
}
}
@@ -175,143 +195,237 @@ $error-color: #f44141 !default;
}
}
- &__body {
- background: #fff;
- border-radius: 4px;
- box-sizing: border-box;
- width: 288px;
+ &__calender-wrap {
+ padding: 16px;
+ height: 248px;
&--large {
- width: 432px;
+ height: 382px;
}
+ }
- &--hastime {
- width: 468px;
- display: flex;
+ &__calender {
+ width: 100%;
+ height: 100%;
+ font-size: 14px;
+ border-collapse: collapse;
+ border-spacing: 0;
+
+ thead th {
+ padding: 2px;
+ }
- .hi-datepicker__body--large {
- width: 611px;
+ &--year,
+ &--month {
+ .hi-datepicker__row {
+ height: 52px;
}
- .hi-timepicker {
- box-shadow: none;
- border-left: 1px solid #f2f2f2;
+ .hi-datepicker__cell {
+ &-text {
+ width: 48px;
+ }
}
}
+ }
- &--period {
- display: flex;
- width: 432px;
+ &__row {
+ &--currentweek {
+ .hi-datepicker__cell {
+ &::before {
+ content: '';
+ background: use-color('primary-20');
+ display: block;
+ height: 24px;
+ position: absolute;
+ left: 0;
+ right: 0;
+ top: 50%;
+ transform: translateY(-50%);
+ }
+ }
}
+ }
- &--range {
- width: 578px;
- display: flex;
- border: none;
-
- &--large {
- width: 866px;
+ &__cell {
+ text-align: center;
+ padding: 4px 0;
+ cursor: pointer;
+ color: rgb(51, 51, 51);
+ position: relative;
- .hi-datepicker__cell.range-se.hi-datepicker__cell--large {
- background-color: #fff;
+ &-text {
+ width: 24px;
+ display: inline-block;
+ height: 24px;
+ line-height: 24px;
+ border-radius: 2px;
+ box-sizing: border-box;
+ margin-left: 50%;
+ transform: translateX(-50%);
+ }
- .hi-datepicker__content__wrap {
- background-color: $basic-color;
+ &__cell:not(.hi-datepicker__cell--range) {
+ &-text {
+ &:hover {
+ background-color: use-color('primary-20');
+ }
+ }
+ }
- .hi-datepicker__text——holiday {
- color: #fff;
+ &--out {
+ color: rgba(44, 48, 78, 0.2);
+ }
- &--work {
- color: #fff;
- }
- }
+ &--today {
+ .hi-datepicker__cell-text {
+ border: 1px solid use-color('primary');
+ }
+ }
- .hi-datepicker__text--showLunar {
- color: #fff;
- }
+ &--range:not(.hi-datepicker__cell--out) {
+ &::before {
+ content: '';
+ background: use-color('primary-20');
+ display: block;
+ height: 24px;
+ position: absolute;
+ left: 0;
+ right: 0;
+ top: 50%;
+ transform: translateY(-50%);
+ }
+ }
- .hi-datepicker__content {
- background: none;
- color: #fff;
- }
- }
+ &--disabled {
+ color: rgba(44, 48, 78, 0.2);
+ cursor: not-allowed;
- &.today {
- background: none;
+ &::before {
+ content: '';
+ display: block;
+ height: 24px;
+ position: absolute;
+ left: 0;
+ right: 0;
+ top: 50%;
+ transform: translateY(-50%);
+ background: rgba(140, 140, 140, 0.1);
+ }
+ }
- .hi-datepicker__content__wrap {
- background-color: $basic-color;
- opacity: 1;
- border: none;
- }
- }
- }
+ &--large {
+ .hi-datepicker__cell-text {
+ width: 48px;
+ height: 48px;
+ line-height: normal;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ font-size: 16px;
}
- .hi-datepicker__panel {
- flex: 1;
+ &.hi-datepicker__cell--range:not(.hi-datepicker__cell--out) {
+ &::before {
+ height: 48px;
+ }
}
}
- &--shortcuts {
- width: 683px;
+ &--selected {
+ .hi-datepicker__cell-text {
+ background: use-color('primary');
+ color: use-color('white');
+ }
- &--large {
- width: 970px;
+ .hi-datepicker__cell--lunar {
+ color: use-color('white');
+ }
- .hi-datepicker__panel {
- width: 432px;
- }
+ .hi-datepicker__lunar {
+ color: use-color('white');
+ }
- .hi-datepicker__cell.range-se.hi-datepicker__cell--large {
- background-color: #fff;
+ .hi-datepicker__mark--rest {
+ color: use-color('white');
+ }
+ }
+ }
- .hi-datepicker__content__wrap {
- background-color: $basic-color;
+ &__cellnum {
+ display: inline-block;
+ transition: all 100ms;
- .hi-datepicker__text——holiday {
- color: #fff;
+ &:hover {
+ transform: scale(1.1);
+ }
+ }
- &--work {
- color: #fff;
- }
- }
+ &__lunar {
+ font-size: 12px;
+ color: rgba(204, 204, 204, 1);
- .hi-datepicker__text--showLunar {
- color: #fff;
- }
+ &--highlight {
+ color: use-color('primary');
+ }
+ }
- .hi-datepicker__content {
- background: none;
- color: #fff;
- }
- }
+ &__footer {
+ height: 48px;
+ border-top: 1px solid use-color('gray-10');
+ box-sizing: border-box;
+ line-height: 48px;
+ text-align: center;
+ cursor: pointer;
+ position: relative;
+ font-size: 14px;
+ flex: 1 1 100%;
- &.today {
- background: none;
+ &:not(.hi-datepicker__footer--disable):hover {
+ color: use-color('primary');
+ }
- .hi-datepicker__content__wrap {
- background-color: $basic-color;
- opacity: 1;
- border: none;
- }
- }
- }
- }
+ &--disable {
+ cursor: not-allowed;
+ color: use-color('gray-50');
}
}
+ &__mask {
+ width: 100%;
+ height: 100%;
+ background: rgba(0, 0, 0, 0.4);
+ position: absolute;
+ top: 0;
+ left: 0;
+ }
+
+ &__timetext {
+ position: absolute;
+ width: 148px;
+ height: 32px;
+ border-radius: 2px;
+ left: 50%;
+ top: 8px;
+ transform: translateX(-50%);
+ background: use-color('white');
+ z-index: 2;
+ line-height: 32px;
+ }
+
&__time-period {
width: 144px;
- border-left: 1px solid #f2f2f2;
+ border-left: 1px solid use-color('gray-10');
text-align: center;
+ box-sizing: border-box;
+ font-size: 14px;
}
&__period-header {
line-height: 48px;
height: 48px;
font-weight: 600;
- border-bottom: 1px solid #f2f2f2;
+ border-bottom: 1px solid use-color('gray-10');
box-sizing: border-box;
}
@@ -333,1107 +447,91 @@ $error-color: #f44141 !default;
}
&--active {
- color: $basic-color;
+ color: use-color('primary');
}
}
- &__footer {
- height: 48px;
- border-top: 1px solid #f2f2f2;
+ &__shortcuts {
+ background: use-color('white');
+ border-right: 1px solid use-color('gray-10');
+ width: 105px;
box-sizing: border-box;
- line-height: 48px;
- text-align: center;
- cursor: pointer;
- position: relative;
+ overflow: hidden;
- &:hover {
- color: $basic-color;
+ &-list {
+ width: 120px;
+ padding: 0;
+ height: 100%;
+ margin: 0;
+ overflow: auto;
+ }
+
+ &-item {
+ cursor: pointer;
+ padding-left: 30px;
+ margin: 21px 0;
+ list-style: none;
+
+ &:hover {
+ color: use-color('primary');
+ }
}
}
- &__mask {
- width: 100%;
- height: 100%;
- background: rgba(0, 0, 0, 0.4);
+ &__mark {
position: absolute;
- top: 0;
- left: 0;
+ font-size: 12px;
+ right: 0;
+ top: -2px;
+
+ &--rest {
+ color: rgba(29, 166, 83, 1);
+ }
+
+ &--work {
+ color: rgba(244, 65, 65, 1);
+ }
}
- &__time-text {
+ &__indiaHoli {
+ display: none;
position: absolute;
- width: 148px;
- height: 32px;
- border-radius: 2px;
- left: 50%;
- top: 8px;
- transform: translateX(-50%);
- background: #fff;
- z-index: 2;
- line-height: 32px;
- }
-
- &__calender-container {
- padding: 17px 18px 15px;
-
- &--date {
- .hi-datepicker__cell {
- &.today:not(.in-range) {
- &:not(.hi-datepicker__cell--large) .hi-datepicker__content__wrap {
- .hi-datepicker__content {
- width: 24px;
- height: 24px;
- box-sizing: border-box;
- }
- }
- }
-
- &.current:not(.in-range) {
- &:not(.hi-datepicker__cell--large) .hi-datepicker__content__wrap {
- .hi-datepicker__content {
- width: 24px;
- height: 24px;
- border-radius: 2px;
- }
- }
- }
- }
-
- .hi-datepicker__cell.in-range:not(.range-se):not(.prev):not(.next) {
- padding: 4px 0;
-
- .hi-datepicker__content__wrap {
- width: 100%;
- background: rgba(66, 142, 245, 0.1);
-
- .hi-datepicker__content {
- background: none;
- }
- }
- }
- }
-
- &--year,
- &--month {
- padding: 10px 5px;
-
- .hi-datepicker__row {
- height: 57px;
- }
-
- .hi-datepicker__cell {
- width: 92px;
- padding-left: 24px;
-
- &.today {
- .hi-datepicker__content__wrap {
- .hi-datepicker__content {
- background: $basic-color;
- opacity: 1;
- color: #fff;
- }
- }
- }
-
- &:not(.hi-datepicker__cell--large) .hi-datepicker__content__wrap {
- width: 48px;
-
- .hi-datepicker__content {
- width: 48px;
- }
- }
- }
- }
-
- &--year {
- .hi-datepicker__cell {
- width: 92px;
- padding-left: 24px;
-
- &--large {
- width: 160px;
- padding-left: 38px;
-
- .hi-datepicker__content__wrap {
- width: 56px;
- }
- }
- }
- }
-
- &--month {
- .hi-datepicker__cell {
- &--large {
- padding-left: 48px;
-
- .hi-datepicker__content__wrap {
- width: 52px;
- height: 24px;
- }
-
- .hi-datepicker__content--showLunar {
- padding-bottom: 0;
- }
- }
- }
- }
-
- &--week {
- .hi-datepicker__row {
- .hi-datepicker__cell:not(.range-se) .hi-datepicker__content__wrap {
- .hi-datepicker__content {
- width: 100%;
- }
-
- // &:hover {
- // .hi-datepicker__content {
- // background: rgba(66, 132, 245, 0.1);
- // }
- // }
- }
-
- &:not(.hi-datepicker__row--current-week):hover {
- // .hi-datepicker__cell:not(.range-se) {
- // padding: 4px 0;
-
- // .hi-datepicker__content__wrap {
- // width: 100%;
- // }
- // }
- td .hi-datepicker__content__wrap {
- background: rgba(66, 132, 245, 0.1);
- }
-
- .hi-datepicker__content__wrap:hover {
- .hi-datepicker__content:hover {
- background: none;
- }
- }
- }
-
- &--current-week {
- .hi-datepicker__cell.range-se.hi-datepicker__cell--large {
- background-color: #fff;
-
- .hi-datepicker__content__wrap {
- background-color: $basic-color;
-
- .hi-datepicker__text——holiday {
- color: #fff;
-
- &--work {
- color: #fff;
- }
- }
-
- .hi-datepicker__text--showLunar {
- color: #fff;
- }
- }
- }
-
- .hi-datepicker__cell.in-range.hi-datepicker__cell--large:not(.range-se) {
- padding: 4px 0;
-
- .hi-datepicker__content__wrap {
- width: 100%;
- }
- }
- }
- }
- }
- }
-
- &__calender {
- border: 0;
- width: 100%;
- border-collapse: separate;
- border-spacing: 0;
-
- th {
- line-height: 24px;
- padding: 0;
- }
- }
-
- &__row {
- &::after {
- content: '';
- }
-
- &--current-week {
- .range-se {
- .hi-datepicker__content {
- background: $basic-color;
- }
- }
- }
- }
-
- // &__content {
- // margin: 0 3px;
- // background: #fff;
- // color: #fff;
- // border-radius: 2px;
- // }
-
- &__cell {
- cursor: pointer;
+ top: 36px;
+ left: 0;
+ width: 430px;
text-align: center;
- padding: 4px 0;
- width: 36px;
-
- &.disabled {
- .hi-datepicker__content {
- cursor: not-allowed;
- color: map-get(get-palette($basic-color), 'g80');
- background: rgba(246, 246, 246, 1);
- }
-
- &:first-child {
- .hi-datepicker__content {
- border-radius: 2px;
- }
- }
-
- &:last-child {
- .hi-datepicker__content {
- border-radius: 2px;
- }
- }
- }
-
- &.hi-datepicker__cell--large {
- position: relative;
-
- .hi-datepicker__content__wrap {
- border-radius: 2px;
-
- &:hover {
- background-color: rgba(66, 142, 245, 0.1);
- }
- }
- }
-
- &:not(.hi-datepicker__cell--large):not(.disabled) {
- .hi-datepicker__content {
- margin: auto;
- width: 24px;
- &:hover {
- background-color: rgba(66, 142, 245, 0.1);
- }
- }
- }
-
- &:not(.in-range) {
- .hi-datepicker__content {
- border-radius: 2px;
- }
- }
-
- &.range-se:not(.prev):not(.next):not(.hi-datepicker__cell--large) {
- .hi-datepicker__content__wrap {
- &:hover {
- background-color: rgba(66, 142, 245, 0.1);
- }
- }
-
- .hi-datepicker__content {
- // margin: 0 3px;
- background: $basic-color;
- color: #fff;
- border-radius: 2px;
- }
- }
-
- &--large {
- width: 48px;
- height: 48px;
- padding: 4px;
-
- &.disabled {
- padding: 4px 0;
-
- .hi-datepicker__content__wrap {
- width: 56px;
- color: #a8aaaf;
- background: #f6f6f6;
- }
- }
-
- &--laster {
- padding: 0 4px;
- }
-
- .hi-datepicker__content__wrap {
- width: 48px;
- height: 48px;
- display: flex;
- flex-direction: column;
- justify-content: center;
- align-items: center;
- }
- }
-
- .hi-datepicker__content--showLunar {
- padding-bottom: 3px;
- overflow: hidden;
+ &-text {
+ display: inline-block;
+ max-width: 410px;
+ word-break: break-all;
+ background: use-color('primary-10');
+ border: 1px solid #d8e5ff;
+ padding: 7px 12px;
+ border-radius: 2px;
}
- .hi-datepicker__text--showLunar {
- font-size: 12px;
- color: rgba(44, 48, 78, 0.2);
-
- &--festival {
- color: $basic-color;
- }
+ &-enter {
+ display: none;
+ opacity: 0;
+ transition: all 0.3s ease-in;
}
- .hi-datepicker__text——holiday {
+ &-enter-done {
display: block;
- width: 0;
- height: 0;
- font-size: 12px;
- position: absolute;
- top: 2px;
- right: 16px;
-
- &--rest {
- color: #1da653;
- }
-
- &--work {
- color: #f63;
- }
- }
-
- // .hi-datepicker__cell--large--laster {
- // .hi-datepicker__text——holiday {
- // top: -28px;
- // right: -26px;
- // }
- // }
-
- &.prev,
- &.next {
- .hi-datepicker__text {
- color: rgba(44, 48, 78, 0.2);
- }
- }
-
- // &.in-range:not(.next):not(.prev) {
- // .hi-datepicker__content {
- // background: rgba(66, 132, 245, 0.1);
- // }
- // }
-
- &.today:not(.in-range) {
- .hi-datepicker__content__wrap {
- .hi-datepicker__content {
- margin: auto;
- border: 1px solid $basic-color;
- color: $basic-color;
- }
-
- .hi-datepicker__text--showLunar {
- color: $basic-color;
- }
- }
- }
-
- &.today:not(.in-range).hi-datepicker__cell--large {
- .hi-datepicker__content__wrap {
- margin: auto;
- border: 1px solid $basic-color;
- opacity: 0.8;
- box-sizing: border-box;
-
- .hi-datepicker__content {
- border: none;
- }
- }
- }
-
- &.current:not(.in-range) {
- &:not(.hi-datepicker__cell--large) .hi-datepicker__content__wrap {
- .hi-datepicker__content {
- margin: auto;
- background-color: $basic-color;
- }
- }
-
- &.hi-datepicker__cell--large {
- .hi-datepicker__content__wrap {
- background-color: $basic-color;
- }
- }
-
- .hi-datepicker__text {
- color: #fff;
- }
-
- .hi-datepicker__text--showLunar {
- color: #fff;
- }
-
- .hi-datepicker__text——holiday {
- color: #fff;
-
- &--work {
- color: #fff;
- }
- }
+ opacity: 1;
+ transition: all 0.3s ease-in;
}
- }
-
- &__text {
- display: inline-block;
- height: 24px;
- line-height: 24px;
- // box-sizing: border-box;
- // border-radius: 2px;
- }
-
- &__shortcuts {
- background: #fff;
- border-right: 1px solid #f2f2f2;
- width: 105px;
- padding: 12px 20px;
- box-sizing: border-box;
- p {
- cursor: pointer;
- line-height: 34px;
- margin: 0;
-
- &:hover {
- color: $basic-color;
- }
+ &-exit {
+ display: block;
+ opacity: 1;
}
- }
-
- .hide {
- display: none !important;
- }
-}
-
-.hi-timepicker {
- width: 180px;
- background: #fff;
- box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.12);
- z-index: 1060;
-
- .hi-timepicker__body {
- width: 180px;
- }
-
- &--timerange {
- width: 400px;
- display: flex;
- justify-content: space-between;
- }
-
- &__split {
- flex: 1;
- height: 248px;
- margin-top: 47px;
- align-self: flex-end;
- border: 1px solid #f2f2f2;
- border-bottom: none;
- }
- &__timeheader {
- display: flex;
- justify-content: space-around;
- height: 48px;
- text-align: center;
- align-items: center;
- font-size: 14px;
- font-weight: 600;
- color: rgba(51, 51, 51, 1);
- border-bottom: 1px solid rgba(242, 242, 242, 1);
- box-sizing: border-box;
- }
-
- &__mark {
- // flex: 1;
- }
-
- &__timebody {
- display: flex;
- height: 248px;
- // border-bottom: 1px solid #f2f2f2;
- position: relative;
- justify-content: space-around;
- box-sizing: border-box;
- overflow: hidden;
- padding: 0;
-
- &::-webkit-scrollbar {
+ &-exit-done {
display: none;
- }
- }
-
- &__page-turn {
- position: absolute;
- z-index: 2;
- width: 16px;
- height: 16px;
- line-height: 16px;
- text-align: center;
- font-size: 16px;
- left: 22px;
- top: 0;
- cursor: pointer;
-
- &:last-child {
- top: auto;
- bottom: 0;
- }
- }
-
- &__list-container {
- width: 60px;
- overflow: hidden;
- position: relative;
- padding: 12px 0;
- }
-
- &__list {
- // position: absolute;
- // z-index: 2;
- width: 80px;
- height: 224px;
- margin: 0;
- list-style-type: none;
- // overflow-x: hidden;
- overflow-y: auto;
- padding: 0;
- user-select: none;
-
- &:nth-child(2) {
- left: 60px;
- z-index: 3;
- }
-
- &:nth-child(3) {
- left: 120px;
- z-index: 4;
- }
- }
-
- &__item {
- text-align: center;
- font-size: 14px;
- color: rgba(0, 0, 0, 0.65);
- line-height: 32px;
- height: 32px;
- width: 60px;
- cursor: pointer;
- box-sizing: border-box;
- // border-right: 1px solid rgba(242, 242, 242, 1);
- background-color: #fff;
-
- &--empty {
- cursor: default;
- }
-
- &--current {
- font-weight: bold;
- }
-
- &--disabled {
- color: rgba(44, 48, 78, 0.2);
- cursor: not-allowed;
- }
-
- &:hover:not(.hi-timepicker__item--current):not(.hi-timepikcer__item--empty):not(.hi-timepicker__item--disabled) {
- background-color: #eff5fe;
- }
- }
-
- &__footer {
- padding: 2px 0;
- text-align: right;
-
- button {
- margin-right: 4px;
- }
- }
-
- &__current-line {
- height: 32px;
- box-sizing: border-box;
- border-top: 1px solid rgba(242, 242, 242, 1);
- border-bottom: 1px solid rgba(242, 242, 242, 1);
-
- // background: rgba(247, 247, 247, 1);
- position: absolute;
- top: 108px;
- left: 0;
- width: 156px;
- z-index: 4;
- pointer-events: none;
- display: flex;
- text-align: center;
- align-items: center;
- margin: 0 12px;
-
- span {
- flex: 1;
- }
- }
-}
-
-@each $key, $value in $palette-primary {
- .theme__#{$key} {
- &.hi-datepicker__input {
- &:hover:not(.hi-datepicker__input--disabled) {
- border: 1px solid map-get(get-palette($value), '50');
- }
-
- &--focus {
- border: 1px solid map-get(get-palette($value), '50');
- }
- }
-
- .hi-timepicker__item:hover:not(.hi-timepicker__item--current):not(.hi-timepikcer__item--empty):not(.hi-timepicker__item--disabled) {
- background-color: rgba($value, 0.1);
- }
- }
-
- .theme__#{$key}.hi-datepicker .hi-datepicker {
- &__time-header {
- &--active {
- color: map-get(get-palette($value), '50');
- }
- }
-
- &__time-footer {
- color: map-get(get-palette($value), '50');
- }
-
- &__calender-container--date .hi-datepicker__cell.in-range:not(.range-se):not(.prev):not(.next) .hi-datepicker__content__wrap {
- width: 100%;
- background: rgba($value, 0.1);
- }
-
- &__shortcuts p:hover {
- color: $value;
- }
-
- &__calender-container--week .hi-datepicker__row:not(.hi-datepicker__row--current-week):hover td .hi-datepicker__content__wrap {
- background: rgba($value, 0.1);
- }
-
- &__header-btns .hi-icon:hover {
- color: $value;
- }
-
- &__header-btns-disabled .hi-icon {
- color: #a8aaaf;
- cursor: not-allowed;
- }
-
- &__header-btns-disabled .hi-icon:hover {
- color: #a8aaaf;
- cursor: not-allowed;
- }
-
- &__period-item--active {
- color: $value;
- }
-
- &__period-item:hover {
- background: rgba($value, 0.1);
- }
-
- &__row {
- &--current-week {
- .hi-datepicker__content {
- background: rgba($value, 0.1);
- }
-
- .range-se {
- .hi-datepicker__content {
- background: map-get(get-palette($value), '50');
- color: #fff;
- border-radius: 2px;
- }
-
- .hi-datepicker__text {
- color: #fff;
- }
- }
-
- .hi-datepicker__cell--large.range-se {
- border-radius: 2px;
- background: map-get(get-palette($value), '50');
-
- .hi-datepicker__content {
- color: #fff;
- }
-
- .hi-datepicker__text {
- color: #fff;
- }
- }
-
- .hi-datepicker__cell.range-se.hi-datepicker__cell--large {
- background-color: #fff;
-
- .hi-datepicker__content__wrap {
- background-color: map-get(get-palette($value), '50');
-
- .hi-datepicker__text——holiday {
- color: #fff;
-
- &--work {
- color: #fff;
- }
- }
-
- .hi-datepicker__text--showLunar {
- color: #fff;
- }
- }
- }
-
- .hi-datepicker__cell.in-range.hi-datepicker__cell--large:not(.range-se) {
- padding: 4px 0;
-
- .hi-datepicker__content__wrap {
- width: 100%;
- }
- }
- }
- }
-
- &__cell {
- &:not(.hi-datepicker__cell--large):not(.disabled) .hi-datepicker__content:hover {
- background-color: rgba($value, 0.1);
- }
-
- &.range-se:not(.prev):not(.next):not(.hi-datepicker__cell--large) .hi-datepicker__content__wrap:hover {
- background-color: rgba($value, 0.1);
- }
-
- &.hi-datepicker__cell--large .hi-datepicker__content__wrap:hover {
- background-color: rgba($value, 0.1);
- }
-
- .hi-datepicker__text——holiday {
- &--rest {
- color: map-get(get-palette($value), '50');
- }
- }
-
- &.in-range:not(.next):not(.prev) {
- .hi-datepicker__content {
- background: rgba($value, 0.1);
- }
-
- .hi-datepicker__content:hover {
- background: rgba($value, 0.1);
- }
- }
-
- &.range-se:not(.next):not(.prev):not(.hi-datepicker__cell--large) {
- .hi-datepicker__content {
- background: map-get(get-palette($value), '50');
- }
- }
-
- &.today:not(.in-range) {
- .hi-datepicker__content__wrap {
- .hi-datepicker__content {
- margin: auto;
- border: 1px solid map-get(get-palette($value), '50');
- color: map-get(get-palette($value), '50');
- }
-
- .hi-datepicker__text--showLunar {
- color: map-get(get-palette($value), '50');
- }
- }
- }
-
- &.today.range-se {
- .hi-datepicker__content__wrap {
- .hi-datepicker__content {
- color: #fff;
- }
-
- .hi-datepicker__text--showLunar {
- color: #fff;
- }
- }
- }
-
- &.today:not(.in-range).hi-datepicker__cell--large {
- .hi-datepicker__content__wrap {
- margin: auto;
- border: 1px solid map-get(get-palette($value), '50');
- opacity: 0.8;
- box-sizing: border-box;
-
- .hi-datepicker__content {
- border: none;
- }
- }
- }
-
- &.current:not(.in-range) {
- &:not(.hi-datepicker__cell--large) .hi-datepicker__content__wrap {
- .hi-datepicker__content {
- margin: auto;
- background-color: map-get(get-palette($value), '50');
- }
- }
-
- &.hi-datepicker__cell--large {
- .hi-datepicker__content__wrap {
- background-color: map-get(get-palette($value), '50');
- }
- }
-
- .hi-datepicker__text {
- color: #fff;
- }
-
- .hi-datepicker__text--showLunar {
- color: #fff;
- }
-
- .hi-datepicker__text——holiday {
- color: #fff;
-
- &--work {
- color: #fff;
- }
- }
- }
- }
-
- &__calender-container--year,
- &__calender-container--month {
- .hi-datepicker__cell {
- &.today:not(.in-range) {
- .hi-datepicker__content__wrap {
- color: map-get(get-palette($value), '50');
- background: #fff;
- margin: inherit;
-
- .hi-datepicker__content {
- box-sizing: border-box;
- border: 1px solid map-get(get-palette($value), '50');
- background-color: #fff;
- color: map-get(get-palette($value), '50');
- }
- }
-
- &.hi-datepicker__cell--large {
- .hi-datepicker__content__wrap {
- box-sizing: border-box;
- border: 1px solid map-get(get-palette($value), '50');
- color: map-get(get-palette($value), '50');
- background: #fff;
- margin: inherit;
-
- .hi-datepicker__text {
- display: inline-block;
- height: 22px;
- // box-sizing: border-box;
- // border-radius: 2px;
- }
-
- .hi-datepicker__content {
- border: none;
- background-color: #fff;
- color: map-get(get-palette($value), '50');
- }
- }
- }
- }
-
- &.today.in-range {
- .hi-datepicker__content__wrap {
- color: map-get(get-palette($value), '50');
- background: #fff;
- margin: inherit;
-
- .hi-datepicker__content {
- color: #333;
- }
- }
- }
-
- &.today.range-se {
- .hi-datepicker__content__wrap {
- color: map-get(get-palette($value), '50');
- background: #fff;
- margin: inherit;
-
- .hi-datepicker__content {
- box-sizing: border-box;
- color: map-get(get-palette($value), '50');
- }
- }
- }
-
- &.today.range-se.in-range {
- .hi-datepicker__content__wrap {
- color: map-get(get-palette($value), '50');
- background: #fff;
- margin: inherit;
-
- .hi-datepicker__content {
- box-sizing: border-box;
- color: #fff;
- }
- }
- }
- }
- }
-
- &__body {
- .hi-datepicker__time-footer {
- color: map-get(get-palette($value), '40');
- }
-
- &--large {
- .hi-datepicker__cell.range-se.hi-datepicker__cell--large {
- background-color: #fff;
-
- .hi-datepicker__content__wrap {
- background-color: map-get(get-palette($value), '40');
-
- .hi-datepicker__text——holiday {
- color: #fff;
-
- &--work {
- color: #fff;
- }
- }
-
- .hi-datepicker__text--showLunar {
- color: #fff;
- }
-
- .hi-datepicker__content {
- background: none;
- color: #fff;
- }
- }
-
- &.today {
- background: none;
-
- .hi-datepicker__content__wrap {
- background-color: map-get(get-palette($value), '40');
- opacity: 1;
- border: none;
- }
- }
- }
- }
-
- &--range {
- width: 578px;
- display: flex;
- border: none;
-
- &--large {
- width: 866px;
-
- .hi-datepicker__cell.range-se.hi-datepicker__cell--large {
- background-color: #fff;
-
- .hi-datepicker__content__wrap {
- background-color: map-get(get-palette($value), '40');
-
- .hi-datepicker__text——holiday {
- color: #fff;
-
- &--work {
- color: #fff;
- }
- }
-
- .hi-datepicker__text--showLunar {
- color: #fff;
- }
-
- .hi-datepicker__content {
- background: none;
- color: #fff;
- }
- }
-
- &.today {
- background: none;
-
- .hi-datepicker__content__wrap {
- background-color: map-get(get-palette($value), '40');
- opacity: 1;
- border: none;
- }
- }
- }
- }
-
- .hi-datepicker__panel {
- flex: 1;
- }
- }
-
- &--shortcuts {
- width: 683px;
-
- &--large {
- width: 970px;
-
- .hi-datepicker__panel {
- width: 432px;
- }
-
- .hi-datepicker__cell.range-se.hi-datepicker__cell--large {
- background-color: #fff;
-
- .hi-datepicker__content__wrap {
- background-color: map-get(get-palette($value), '40');
-
- .hi-datepicker__text——holiday {
- color: #fff;
-
- &--work {
- color: #fff;
- }
- }
-
- .hi-datepicker__text--showLunar {
- color: #fff;
- }
-
- .hi-datepicker__content {
- background: none;
- color: #fff;
- }
- }
-
- &.today {
- background: none;
-
- .hi-datepicker__content__wrap {
- background-color: map-get(get-palette($value), '40') !important;
- opacity: 1;
- border: none;
- }
- }
- }
- }
- }
+ opacity: 0;
+ transition: all 0.3s ease-in;
}
}
}
diff --git a/components/date-picker/style/timepicker.js b/components/date-picker/style/timepicker.js
new file mode 100644
index 000000000..36dc5c486
--- /dev/null
+++ b/components/date-picker/style/timepicker.js
@@ -0,0 +1 @@
+import './timepicker.scss'
diff --git a/components/date-picker/style/timepicker.scss b/components/date-picker/style/timepicker.scss
index 82d0321ac..e5af63da0 100644
--- a/components/date-picker/style/timepicker.scss
+++ b/components/date-picker/style/timepicker.scss
@@ -1,35 +1,149 @@
+@import '@hi-ui/core-css/index.scss';
+@import '../../core-css/index.scss';
+
.hi-timepicker {
width: 180px;
- background: #fff;
+ background: use-color('white');
+ box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.12);
+ z-index: 1060;
+
+ &__popper {
+ width: 180px;
+ height: 296px;
+ background: use-color('white');
+ box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.15);
+ border-radius: 4px;
+
+ &--range {
+ width: 400px;
+ }
+ }
+
+ &__panel {
+ background: use-color('white');
+
+ &--range {
+ width: 400px;
+ display: flex;
+ justify-content: space-between;
+ }
+ }
+
+ &__body {
+ width: 180px;
+ }
+
+ &__split {
+ flex: 1;
+ height: 248px;
+ margin-top: 47px;
+ align-self: flex-end;
+ border: 1px solid use-color('gray-10');
+ border-bottom: none;
+ }
+
+ &__timeheader {
+ display: flex;
+ justify-content: space-around;
+ height: 48px;
+ text-align: center;
+ align-items: center;
+ font-size: 14px;
+ font-weight: $font-weight-bold;
+ color: use-color('black');
+ border-bottom: 1px solid use-color('gray-10');
+ box-sizing: border-box;
+ }
&__timebody {
display: flex;
+ height: 248px;
+ position: relative;
justify-content: space-around;
- height: 195px;
- border-bottom: 1px solid #f2f2f2;
+ box-sizing: border-box;
+ overflow: hidden;
+ padding: 0;
+
+ &::-webkit-scrollbar {
+ display: none;
+ }
}
- &__time-list {
- overflow: auto;
- flex: 1;
+ &__page-turn {
+ position: absolute;
+ z-index: 2;
+ width: 16px;
+ height: 16px;
+ line-height: 16px;
+ text-align: center;
+ font-size: 16px;
+ left: 22px;
+ top: 0;
+ cursor: pointer;
- &-el {
- text-align: center;
- font-size: 14px;
- color: rgba(0, 0, 0, 0.65);
- line-height: 22px;
- padding: 8px;
- border-right: 1px solid #f2f2f2;
- cursor: pointer;
+ &:last-child {
+ top: auto;
+ bottom: 0;
+ }
+ }
+
+ &__list-container {
+ width: 60px;
+ overflow: hidden;
+ position: relative;
+ padding: 12px 0;
+ }
+
+ &__list {
+ width: 80px;
+ height: 224px;
+ margin: 0;
+ list-style-type: none;
+ overflow-y: auto;
+ padding: 0;
+ user-select: none;
+
+ &:nth-child(2) {
+ left: 60px;
+ z-index: 3;
+ }
- &_current {
- background: #f7f7f7;
- font-weight: bold;
- }
+ &:nth-child(3) {
+ left: 120px;
+ z-index: 4;
}
}
- &__time-footer {
+ &__item {
+ text-align: center;
+ font-size: 14px;
+ color: use-color('gray-80');
+ line-height: 32px;
+ height: 32px;
+ width: 60px;
+ cursor: pointer;
+ box-sizing: border-box;
+ background-color: use-color('white');
+
+ &--empty {
+ cursor: default;
+ }
+
+ &--current {
+ font-weight: bold;
+ }
+
+ &--disabled {
+ color: rgba(44, 48, 78, 0.2);
+ cursor: not-allowed;
+ }
+
+ &:hover:not(.hi-timepicker__item--current):not(.hi-timepikcer__item--empty):not(.hi-timepicker__item--disabled) {
+ background-color: use-color('primary-10');
+ }
+ }
+
+ &__footer {
padding: 2px 0;
text-align: right;
@@ -37,4 +151,25 @@
margin-right: 4px;
}
}
+
+ &__current-line {
+ height: 32px;
+ box-sizing: border-box;
+ border-top: 1px solid use-color('gray-10');
+ border-bottom: 1px solid use-color('gray-10');
+ position: absolute;
+ top: 108px;
+ left: 0;
+ width: 156px;
+ z-index: 4;
+ pointer-events: none;
+ display: flex;
+ text-align: center;
+ align-items: center;
+ margin: 0 12px;
+
+ span {
+ flex: 1;
+ }
+ }
}
diff --git a/components/date-picker/util.js b/components/date-picker/util.js
deleted file mode 100644
index 3088eb3ed..000000000
--- a/components/date-picker/util.js
+++ /dev/null
@@ -1,103 +0,0 @@
-import request from 'axios'
-import { addMonths, getDay, subDays, differenceInDays, startOfWeek, endOfWeek, getYear, getMonth, toDate, isValid } from './dateUtil'
-const holiday = {
- PRCHoliday: 'https://cdn.cnbj1.fds.api.mi-img.com/hiui/PRCHoliday.json?',
- PRCLunar: 'https://cdn.cnbj1.fds.api.mi-img.com/hiui/PRCLunar.json?',
- IndiaHoliday: 'https://cdn.cnbj1.fds.api.mi-img.com/hiui/IndiaHoliday.json?'
-}
-
-export const deconstructDate = (date, weekOffset = 0) => {
- !(date instanceof Date) && (date = new Date(date))
- return {
- year: date.getFullYear(),
- month: date.getMonth() + 1,
- week: getYearWeek(date, weekOffset).weekNum,
- day: date.getDate(),
- hours: date.getHours(),
- minutes: date.getMinutes(),
- seconds: date.getSeconds(),
- time: date.getTime()
- }
-}
-export const getYearWeek = (date, weekStart = 0) => {
- const year = date.getFullYear()
- let date1 = new Date(year, parseInt(date.getMonth()), date.getDate())
- let date2 = new Date(year, 0, 1)
- let num = getDay(date2)
- date2 = subDays(date2, weekStart ? (num - 1) : num) // 周日开始
- const din = differenceInDays(date1, date2)
- return {
- weekNum: Math.ceil((din + 1) / 7),
- start: startOfWeek(date1, {weekStartsOn: weekStart}),
- end: endOfWeek(date1, {weekStartsOn: weekStart})
- }
-}
-
-export const nextMonth = function (date) {
- !(date instanceof Date) && (date = new Date(date))
- return addMonths(date, 1)
-}
-
-// 判读日期是不可点击
-export const colDisabled = (type, col, _year, props, y) => {
- const {max: maxDate, min: minDate} = props
- const maxYear = maxDate && getYear(maxDate)
- const minYear = minDate && getYear(minDate)
- if (type === 'year') {
- col.text = y
- if (minDate || maxDate) {
- col.disabled = (y > maxYear || y < minYear)
- }
- } else {
- col.text = props.localeDatas.datePicker.month[y - 1]
-
- if (minDate || maxDate) {
- col.disabled = (_year > maxYear || _year < minYear)
-
- if (_year === minYear) {
- col.disabled = y - 1 < getMonth(minDate)
- }
- if (_year === maxYear && !col.disabled) {
- col.disabled = y - 1 > getMonth(maxDate)
- }
- }
- }
- return col
-}
-/**
- * 是否展示历法次要信息
- * @param {Object} props
- */
-export const showLargeCalendar = (props) => {
- return (props.type !== 'yearrange' && props.type !== 'monthrange') && (props.altCalendar || props.altCalendarPreset || props.dateMarkRender || props.dateMarkPreset)
-}
-
-export const getPRCDate = (api) => {
- const url = holiday[api]
- let options = {
- url,
- method: 'GET'
- }
- return url ? request.create().request(options) : null
-}
-
-// 处理输入不在该范围内的处理
-export const getInRangeDate = (startDate, endDate, max, min) => {
- let _startDate = isValid(startDate) ? startDate : ''
- let _endDate = isValid(endDate) ? endDate : ''
- if (min && isValid(startDate)) {
- const minTimestamp = Date.parse(toDate(min))
- const startDateTimestamp = Date.parse(startDate)
- const endDateTimestamp = Date.parse(endDate)
- _startDate = startDateTimestamp < minTimestamp ? new Date(minTimestamp) : new Date(startDate)
- _endDate = endDateTimestamp < minTimestamp ? new Date(minTimestamp) : new Date(endDate)
- }
- if (max && isValid(startDate)) {
- const maxTimestamp = Date.parse(toDate(max))
- const startDateTimestamp = Date.parse(_startDate)
- const endDateTimestamp = Date.parse(_endDate)
- _startDate = startDateTimestamp > maxTimestamp ? new Date(maxTimestamp) : new Date(_startDate)
- _endDate = endDateTimestamp > maxTimestamp ? new Date(maxTimestamp) : new Date(_endDate)
- }
- return {startDate: _startDate, endDate: _endDate}
-}
diff --git a/components/date-picker/utils.js b/components/date-picker/utils.js
new file mode 100644
index 000000000..32becc485
--- /dev/null
+++ b/components/date-picker/utils.js
@@ -0,0 +1,219 @@
+import React from 'react'
+import _ from 'lodash'
+import moment from 'moment'
+import request from 'axios'
+import Lunar from './toLunar'
+const holiday = {
+ PRCHoliday: 'https://cdn.cnbj1.fds.api.mi-img.com/hiui/PRCHoliday.json?',
+ PRCLunar: 'https://cdn.cnbj1.fds.api.mi-img.com/hiui/PRCLunar.json?',
+ IndiaHoliday: 'https://cdn.cnbj1.fds.api.mi-img.com/hiui/IndiaHoliday.json?'
+}
+export const deconstructDate = (date) => {
+ return {
+ year: date.year(),
+ month: date.month(),
+ date: date.date(),
+ hour: date.hour(),
+ minute: date.minute(),
+ second: date.second()
+ }
+}
+/**
+ * 获取 yearrange monthrange 类型
+ * @param {String} type
+ */
+export const getView = (type) => {
+ return type.includes('year') || type.includes('month') ? type.split('range')[0] : 'date'
+}
+
+/**
+ *
+ * @param {Array} currentDates 原始数组
+ * @param {Moment} newDate 新时间
+ * @param {Number} index 更新数组位置
+ */
+export const genNewDates = (currentDates, newDate, index = 0) => {
+ const newDates = _.cloneDeep(currentDates)
+ newDates[index] = newDate
+ return newDates
+}
+
+/**
+ * 生成 renderDates 用于 Calender(Range) 渲染时的基础时间
+ * @param {Array} dates 原始数据
+ * @param {String} type 选择器类型
+ */
+export const parseRenderDates = (dates, type) => {
+ let [leftDate, rightDate] = _.cloneDeep(dates)
+ const getRightDate = () => {
+ if (type === 'yearrange') {
+ if (!rightDate) {
+ return moment(leftDate).add(12, 'year')
+ }
+ const diff = rightDate.year() - leftDate.year()
+ if (diff <= 12) {
+ return moment(leftDate).add(12, 'year')
+ }
+ const rYear = rightDate.year()
+ const nRightDate = moment(rightDate).year(rYear + (12 - (diff % 12)), 'year')
+ return nRightDate
+ }
+ if (type === 'monthrange') {
+ return moment(leftDate).add(1, 'year')
+ }
+ if (!rightDate || leftDate.isSame(rightDate, 'month')) {
+ return moment(leftDate).add(1, 'months')
+ }
+ return rightDate
+ }
+
+ leftDate = leftDate || moment()
+ rightDate = getRightDate()
+ return [leftDate, rightDate]
+}
+
+export const getTimePeriodData = (timeInterval) => {
+ const segment = (24 * 60) / timeInterval
+ let pre = 0
+ let next = 0
+ const periodData = []
+ const func = (val) => (val < 10 ? '0' + val : val)
+ for (let i = 0; i < segment; i++) {
+ next += timeInterval
+ const timeStart = func(parseInt(pre / 60))
+ const timeEnd = func(parseInt(next / 60))
+ const timeStr = timeStart + ':' + func(pre % 60) + ' ~ ' + timeEnd + ':' + func(next % 60)
+ periodData.push({
+ timeStr,
+ timeStart,
+ timeEnd
+ })
+ pre = next
+ }
+ return periodData
+}
+
+/**
+ * 是否展示历法次要信息
+ * @param {Object} props
+ */
+export const showLargeCalendar = (props) => {
+ return props.altCalendar || props.altCalendarPreset || props.dateMarkRender || props.dateMarkPreset
+}
+
+export const getPRCDate = (api) => {
+ const url = holiday[api]
+ const options = {
+ url,
+ method: 'GET'
+ }
+ return url ? request.create().request(options) : null
+}
+
+const altCalendarText = (datainfo, lunarcellinfo, altCalendarPresetData) => {
+ if (altCalendarPresetData && altCalendarPresetData[datainfo]) {
+ return altCalendarPresetData[datainfo].text || altCalendarPresetData[datainfo]
+ }
+ return lunarcellinfo.text
+}
+const getMarkNode = (node) => {
+ return {node}
+}
+const markRender = (datainfo, dateMarkRender, dateMarkPresetData) => {
+ // 存在传入自定就优先使用自定义
+ const markRenderNode = dateMarkRender ? dateMarkRender(new Date(datainfo).getTime(), new Date().getTime()) : false
+ if (markRenderNode) {
+ return getMarkNode(markRenderNode)
+ }
+ if (dateMarkPresetData) {
+ return dateMarkPresetData[datainfo]
+ }
+ return null
+}
+/**
+ * 获取完整时间
+ * @param {*} value 日
+ * @param {*} cls className
+ */
+export const getFullTime = ({
+ cell,
+ renderDate,
+ altCalendarPreset,
+ altCalendar,
+ dateMarkRender,
+ altCalendarPresetData,
+ dateMarkPresetData
+}) => {
+ if (cell.type === 'disabled') return false
+ const newDate = moment(renderDate)
+ newDate.date(cell.value)
+ if (cell.type === 'prev') {
+ newDate.subtract(1, 'months')
+ }
+ if (cell.type === 'next') {
+ newDate.add(1, 'months')
+ }
+ const _year = newDate.year()
+ const _month = newDate.month() + 1
+ const _value = cell.value
+ const datainfo = _year + '/' + _month + '/' + _value
+ const LunarInfo = Lunar.toLunar(_year, _month, _value)
+ let lunarcellinfo = {
+ text: altCalendarPreset === 'zh-CN' ? LunarInfo[6] : null, // 默认预置信息
+ highlight: false
+ }
+ if (altCalendar || dateMarkRender) {
+ lunarcellinfo = {
+ text: altCalendarText(datainfo, lunarcellinfo, altCalendarPresetData),
+ highlight: altCalendarPresetData && altCalendarPresetData[datainfo] && altCalendarPresetData[datainfo].highlight,
+ nodeMark: markRender(datainfo, dateMarkRender, dateMarkPresetData)
+ }
+ }
+ if (
+ (dateMarkPresetData && dateMarkPresetData[datainfo]) ||
+ (altCalendarPresetData && altCalendarPresetData[datainfo])
+ ) {
+ lunarcellinfo = {
+ text: altCalendarText(datainfo, lunarcellinfo, altCalendarPresetData),
+ highlight: altCalendarPresetData && altCalendarPresetData[datainfo] && altCalendarPresetData[datainfo].highlight,
+ nodeMark: markRender(datainfo, dateMarkRender, dateMarkPresetData)
+ }
+ }
+ return { ...altCalendarPresetData[datainfo], ...lunarcellinfo }
+}
+
+/**
+ * 处理输入不在该范围内的处理
+ * @param {String} startDate 开始日期
+ * @param {String} endDate 结束日期
+ * @param {String} max 最大日期
+ * @param {String} min 最小日期
+ */
+export const getInRangeDate = (momentstartDate, momentendDate, max, min) => {
+ const startDate = momentstartDate && momentstartDate.format()
+ const endDate = momentendDate && momentendDate.format()
+
+ const isValid = (value) => {
+ return moment(value).isValid()
+ }
+ const toDate = (value) => {
+ return moment(value).format()
+ }
+ let _startDate = isValid(startDate) ? startDate : ''
+ let _endDate = isValid(endDate) ? endDate : ''
+ if (min && isValid(startDate)) {
+ const minTimestamp = Date.parse(toDate(min))
+ const startDateTimestamp = Date.parse(startDate)
+ const endDateTimestamp = Date.parse(endDate)
+ _startDate = startDateTimestamp < minTimestamp ? new Date(minTimestamp) : new Date(startDate)
+ _endDate = endDateTimestamp < minTimestamp ? new Date(minTimestamp) : new Date(endDate)
+ }
+ if (max && isValid(startDate)) {
+ const maxTimestamp = Date.parse(toDate(max))
+ const startDateTimestamp = Date.parse(_startDate)
+ const endDateTimestamp = Date.parse(_endDate)
+ _startDate = startDateTimestamp > maxTimestamp ? new Date(maxTimestamp) : new Date(_startDate)
+ _endDate = endDateTimestamp > maxTimestamp ? new Date(maxTimestamp) : new Date(_endDate)
+ }
+ return { startDate: _startDate, endDate: _endDate }
+}
diff --git a/components/drawer/index.d.ts b/components/drawer/index.d.ts
new file mode 100644
index 000000000..6417f7d67
--- /dev/null
+++ b/components/drawer/index.d.ts
@@ -0,0 +1,15 @@
+import { CSSProperties } from "react"
+
+interface Props {
+ title?: string | JSX.Element
+ visible?: boolean
+ closable?: boolean
+ maskClosable?: boolean
+ showMask?: boolean
+ width?: number
+ footer?: JSX.Element
+ placement?: 'left' | 'right'
+ onClose?: (e: MouseEvent) => void
+}
+declare const Drawer: React.ComponentType
+export default Drawer
diff --git a/components/drawer/index.jsx b/components/drawer/index.jsx
new file mode 100644
index 000000000..d310d16db
--- /dev/null
+++ b/components/drawer/index.jsx
@@ -0,0 +1,74 @@
+import React, { useRef, useEffect } from 'react'
+import ReactDOM from 'react-dom'
+import Icon from '../icon'
+import Classnames from 'classnames'
+import './style/index'
+
+const PREFIX = 'hi-drawer'
+
+const getDefaultContainer = () => {
+ const defaultContainer = document.createElement('div')
+ document.body.appendChild(defaultContainer)
+ return defaultContainer
+}
+
+const DrawerComp = ({
+ children,
+ container,
+ visible,
+ title,
+ onClose,
+ maskClosable = true,
+ closable = true,
+ footer,
+ width,
+ showMask = true,
+ placement = 'right'
+}) => {
+ // TODO: 整体可以抽成一个 hooks 供 modal 和 drawer 复用
+ const defaultContainer = useRef(false)
+ if (defaultContainer.current === false) {
+ defaultContainer.current = getDefaultContainer()
+ }
+
+ useEffect(() => {
+ const parent = (container || defaultContainer.current).parentNode
+ // 屏蔽滚动条
+ if (visible) {
+ parent.style.setProperty('overflow', 'hidden')
+ } else {
+ parent.style.removeProperty('overflow')
+ }
+ }, [visible, container])
+
+ return ReactDOM.createPortal(
+
+ {showMask && (
+
{
+ if (maskClosable) {
+ onClose()
+ }
+ }}
+ />
+ )}
+
+
+ {title}
+ {closable && }
+
+
{children}
+ {footer &&
{footer}
}
+
+
,
+ container || defaultContainer.current
+ )
+}
+
+export default DrawerComp
diff --git a/components/confirm/style/index.js b/components/drawer/style/index.js
similarity index 100%
rename from components/confirm/style/index.js
rename to components/drawer/style/index.js
diff --git a/components/drawer/style/index.scss b/components/drawer/style/index.scss
new file mode 100644
index 000000000..0d9c774a5
--- /dev/null
+++ b/components/drawer/style/index.scss
@@ -0,0 +1,82 @@
+@import '../../core-css/index.scss';
+
+$drawer: 'hi-drawer' !default;
+
+.#{$drawer} {
+ color: use-color('gray-80');
+ font-size: $font-size-normal;
+
+ &__mask {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ height: 0;
+ z-index: 1000;
+ background: rgba(0, 0, 0, 0.45);
+ opacity: 0;
+ transition: opacity 0.3s, height 0s 0.3s;
+
+ &--visible {
+ opacity: 1;
+ height: 100%;
+ transition: opacity 0.3s;
+ }
+ }
+
+ &__wrapper {
+ width: 360px;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ background: use-color('white');
+ z-index: 1000;
+ top: 0;
+ position: fixed;
+ border-radius: 2px;
+ box-shadow: 0 5px 15px 0 rgba(0, 0, 0, 0.1);
+ transform: translateX(100%);
+ transition: transform 0.3s;
+
+ &--left {
+ left: 0;
+ transform: translateX(-100%);
+ }
+
+ &--right {
+ right: 0;
+ transform: translateX(100%);
+ }
+
+ &--visible {
+ transform: none;
+ }
+ }
+
+ &__header {
+ font-size: 16px;
+ color: use-color('gray-80');
+ font-weight: 500;
+ height: 54px;
+ border-bottom: 1px solid use-color('gray-30');
+ box-sizing: border-box;
+ padding: 0 24px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ }
+
+ &__content {
+ box-sizing: border-box;
+ overflow: auto;
+ flex: 1;
+ padding: 24px;
+ }
+
+ &__footer {
+ border-top: 1px solid use-color('gray-30');
+ padding: 12px;
+ flex-shrink: 0;
+ }
+}
diff --git a/components/dropdown/Dropdown.jsx b/components/dropdown/Dropdown.jsx
index 093a87da5..6bfcc1402 100644
--- a/components/dropdown/Dropdown.jsx
+++ b/components/dropdown/Dropdown.jsx
@@ -6,7 +6,11 @@ import DropdownMenu, { propTypesOfMenuData } from './DropdownMenu'
import Provider from '../context'
import { prefixCls } from '.'
-import { getIsTriggerEqualHover, getIsTriggerEqualContextmenu, trimTriggers } from './utils'
+import {
+ getIsTriggerEqualHover,
+ getIsTriggerEqualContextmenu,
+ trimTriggers
+} from './utils'
class Dropdown extends React.Component {
refDropdown = React.createRef()
timerHideMenu = null
@@ -22,31 +26,33 @@ class Dropdown extends React.Component {
}
}
}
- setPopperShow = (event) => {
- event.preventDefault()
+ setPopperShow = event => {
+ this.eventHandler(event)
clearTimeout(this.timerHideMenu)
this.setState({ visible: true })
}
- setPopperHide = (event) => {
- event.preventDefault()
+ setPopperHide = event => {
+ this.eventHandler(event)
this.setState({ visible: false })
}
- setPopperDelayHide = (event) => {
+ setPopperDelayHide = event => {
this.eventHandler(event)
this.timerHideMenu = setTimeout(() => {
this.setState({ visible: false })
}, 100)
}
+
toggleVisible = () => {
const { visible } = this.state
const toggleVisible = visible ? this.setPopperHide : this.setPopperShow
return toggleVisible
}
+
getPopperShowHandler = () => {
const { disabled } = this.props
if (disabled) return {}
const triggers = trimTriggers(this.props)
- const getHandler = (props) => {
+ const getHandler = props => {
if (getIsTriggerEqualHover(props)) {
return {
onMouseEnter: this.setPopperShow
@@ -61,14 +67,19 @@ class Dropdown extends React.Component {
onClick: this.toggleVisible()
}
}
- return triggers.reduce((prev, cur) => Object.assign(prev, getHandler(cur)), {})
+ return triggers.reduce(
+ (prev, cur) => Object.assign(prev, getHandler(cur)),
+ {}
+ )
}
getPopperHideHandler = () => {
const { disabled } = this.props
if (disabled) return {}
- return getIsTriggerEqualHover(this.props) ? {
- onMouseLeave: this.setPopperDelayHide
- } : {}
+ return getIsTriggerEqualHover(this.props)
+ ? {
+ onMouseLeave: this.setPopperDelayHide
+ }
+ : {}
}
handleMenuMouseLeave = () => {
getIsTriggerEqualHover(this.props) && this.setPopperDelayHide()
@@ -90,23 +101,32 @@ class Dropdown extends React.Component {
onClick && onClick(data)
}
- handleDocumentClick = (e) => {
- if (this.refDropdown.current) {
- !this.refDropdown.current.contains(e.target) && this.setState({ visible: false })
- }
- }
- componentWillUnmount () {
- document.removeEventListener('click', this.handleDocumentClick)
- document.removeEventListener('contextmenu', this.handleDocumentClick)
- }
- componentDidMount () {
- document.addEventListener('click', this.handleDocumentClick)
- document.addEventListener('contextmenu', this.handleDocumentClick)
+
+ handleDocumentClick = e => {
+ this.setState({ visible: false })
}
+
render () {
- const { className, style, title, type, placement, data, disabled, width, onButtonClick, theme } = this.props
+ const {
+ className,
+ style,
+ title,
+ type,
+ placement,
+ data,
+ disabled,
+ width,
+ onButtonClick,
+ theme,
+ overlayClassName
+ } = this.props
const { visible } = this.state
- const dropdownCls = classNames(prefixCls, prefixCls + '--' + type, className, disabled && `${prefixCls}--disabled`)
+ const dropdownCls = classNames(
+ prefixCls,
+ prefixCls + '--' + type,
+ className,
+ disabled && `${prefixCls}--disabled`
+ )
return (
{isGroup && (
)}
diff --git a/components/dropdown/DropdownMenu.jsx b/components/dropdown/DropdownMenu.jsx
index c8b14947f..68b81ca43 100644
--- a/components/dropdown/DropdownMenu.jsx
+++ b/components/dropdown/DropdownMenu.jsx
@@ -6,7 +6,7 @@ import { prefixCls } from '.'
import DropdownMenuItem from './DropdownMenuItem'
class DropdownMenu extends React.Component {
- render () {
+ render() {
const {
data,
attachEle,
@@ -17,8 +17,10 @@ class DropdownMenu extends React.Component {
onChildMenuMouseEnter,
onChildMenuMouseLeave,
onMenuItemClick,
+ handleDocumentClick,
width,
- theme
+ theme,
+ overlayClassName
} = this.props
const menuCls = classNames(`${prefixCls}__menu`, `theme__${theme}`)
return (
@@ -26,12 +28,17 @@ class DropdownMenu extends React.Component {
className={`${prefixCls}__popper`}
show={visible}
attachEle={attachEle}
+ // container={document.body}
zIndex={1060}
placement={placement}
width={width}
onMouseEnter={onMouseEnter}
leftGap={0}
+ overlayClassName={overlayClassName}
onMouseLeave={onMouseLeave}
+ onClickOutside={() => {
+ handleDocumentClick && handleDocumentClick()
+ }}
>
{data.map((item, index) => (
diff --git a/components/dropdown/DropdownMenuItem.jsx b/components/dropdown/DropdownMenuItem.jsx
index 4a8853e3a..909e74fc0 100644
--- a/components/dropdown/DropdownMenuItem.jsx
+++ b/components/dropdown/DropdownMenuItem.jsx
@@ -4,19 +4,27 @@ import Icon from '../icon'
import DropdownMenu from './DropdownMenu'
import { prefixCls } from '.'
-const MenuItemWrapper = forwardRef(({ href, children, disabled, ...props }, ref) => {
- const shouldUseLink = href && !disabled
- if (disabled) {
- Reflect.deleteProperty(props, 'onMouseEnter')
- Reflect.deleteProperty(props, 'onMouseLeave')
- Reflect.deleteProperty(props, 'onClick')
+const MenuItemWrapper = forwardRef(
+ ({ href, target, children, disabled, ...props }, ref) => {
+ const shouldUseLink = href && !disabled
+ if (disabled) {
+ Reflect.deleteProperty(props, 'onMouseEnter')
+ Reflect.deleteProperty(props, 'onMouseLeave')
+ Reflect.deleteProperty(props, 'onClick')
+ }
+ return (
+ -
+ {shouldUseLink ? (
+
+ {children}
+
+ ) : (
+ children
+ )}
+
+ )
}
- return (
- -
- {shouldUseLink ? {children} : children}
-
- )
-})
+)
export default class DropdownMenuItem extends React.Component {
refItem = React.createRef()
@@ -51,18 +59,14 @@ export default class DropdownMenuItem extends React.Component {
this.setMenuHide()
}
- handleMenuItemClick = (event) => {
+ handleMenuItemClick = event => {
if (event) {
event.stopPropagation()
- event.preventDefault()
- if (event.nativeEvent && event.nativeEvent.stopImmediatePropagation) {
- event.nativeEvent.stopImmediatePropagation()
- }
}
const { onMenuItemClick, id, children, href } = this.props
- onMenuItemClick(id, (href || !children))
+ onMenuItemClick(id, href || !children)
}
- render () {
+ render() {
const {
title,
children,
diff --git a/components/dropdown/index.d.ts b/components/dropdown/index.d.ts
new file mode 100644
index 000000000..fc5e7b56c
--- /dev/null
+++ b/components/dropdown/index.d.ts
@@ -0,0 +1,27 @@
+import { CSSProperties } from "react"
+
+type DataItem = {
+ title: string | JSX.Element
+ id: string | number
+ disabled?: boolean
+ href?: string
+}
+interface TriggersArray {
+ [index: number]: 'click' | 'contextmenu' | 'hover'
+}
+interface Props {
+ trigger?: TriggersArray[number] | TriggersArray
+ data: DataItem[]
+ title: string | JSX.Element
+ type?: 'text' | 'button' | 'group'
+ placement?: 'bottom-start' | 'top-start' | 'bottom' | 'top'
+ disabled?: boolean
+ width?: number
+ className?: string
+ style?: CSSProperties
+ onClick?: (id: string | number) => void
+ onButtonClick?: (event: MouseEvent) => void
+ overlayClassName?: string
+}
+declare const Dropdown: React.ComponentType
+export default Dropdown
diff --git a/components/dropdown/index.js b/components/dropdown/index.js
index ae7e4dae2..f0058c567 100644
--- a/components/dropdown/index.js
+++ b/components/dropdown/index.js
@@ -6,29 +6,45 @@ export const prefixCls = 'hi-dropdown'
const CompatedDropdown = forwardRef(({ prefix, suffix, data, list, ...props }, ref) => {
let originData = []
- originData = (list && !data) ? [...list] : [...data]
+ originData = list && !data ? [...list] : [...data]
originData = convertData(originData, prefix, suffix)
return
})
export default CompatedDropdown
-function convertData (data, prefix = '', suffix = '') {
- const recur = data => {
- return data.map(item => {
+function convertData(data, prefix = '', suffix = '') {
+ const recur = (data) => {
+ return data.map((item) => {
if (item.children) {
item.children = recur(item.children)
}
if (item.title !== '-') {
if (item.prefix) {
- item.title = {item.prefix} {item.title}
+ item.title = (
+
+ {item.prefix} {item.title}
+
+ )
} else {
- item.title = {prefix} {item.title}
+ item.title = (
+
+ {prefix} {item.title}
+
+ )
}
if (item.suffix && !item.children) {
- item.title = {item.title} {item.suffix}
+ item.title = (
+
+ {item.title} {item.suffix}
+
+ )
} else {
- item.title = {item.title} {suffix}
+ item.title = (
+
+ {item.title} {suffix}
+
+ )
}
}
if (item.url && !item.href) {
diff --git a/components/dropdown/style/index.scss b/components/dropdown/style/index.scss
index d775eeb0c..f9a4a6432 100644
--- a/components/dropdown/style/index.scss
+++ b/components/dropdown/style/index.scss
@@ -1,4 +1,4 @@
-@import '@hi-ui/core-css/index.scss';
+@import '../../core-css/index.scss';
$prefixCls: 'hi-dropdown' !default;
@@ -15,14 +15,12 @@ $prefixCls: 'hi-dropdown' !default;
}
.#{$prefixCls} {
- @include component-reset();
-
user-select: none;
display: inline-block;
&--disabled {
.#{$prefixCls}__button {
- color: #ccc;
+ color: use-color('gray-50');
cursor: not-allowed;
}
}
@@ -70,13 +68,13 @@ $prefixCls: 'hi-dropdown' !default;
}
&__popper {
- background: rgba(255, 255, 255, 1);
+ background: use-color('white');
box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.15);
border-radius: 2px;
}
&__divider {
- background-color: #e8e8e8;
+ background-color: use-color('gray-20');
width: 100%;
height: 1px;
margin: 4px 0;
@@ -100,26 +98,18 @@ $prefixCls: 'hi-dropdown' !default;
transition: 0.3s ease;
&--disabled {
- color: #ccc;
+ color: use-color('gray-50');
cursor: not-allowed;
}
a {
- color: #333;
+ color: use-color('black');
width: 100%;
}
&:not(.hi-dropdown__menu-item--disabled):hover {
- background-color: #ecf2fe;
+ background-color: use-color('primary-20');
}
}
}
}
-
-@each $key, $value in $palette-primary {
- .theme__#{$key}.#{$prefixCls}__menu {
- .#{$prefixCls}__menu-item:not(.hi-dropdown__menu-item--disabled):hover {
- background-color: rgba($value, 0.15);
- }
- }
-}
diff --git a/components/filter/index.d.ts b/components/filter/index.d.ts
new file mode 100644
index 000000000..042916248
--- /dev/null
+++ b/components/filter/index.d.ts
@@ -0,0 +1,16 @@
+type DataItem = {
+ id?: string | number
+ content?: string | JSX.Element
+ disabled?: boolean
+}
+export interface Props {
+ label?: string[]
+ labelWidth?: number
+ showUnderline?: boolean
+ data?: DataItem[]
+ defaultValue?: string[] | number[]
+ value?: string[] | number[]
+ onChange?: (value: number | string) => void
+}
+declare const Filter: React.ComponentType
+export default Filter
diff --git a/components/filter/index.js b/components/filter/index.js
new file mode 100644
index 000000000..60389e005
--- /dev/null
+++ b/components/filter/index.js
@@ -0,0 +1,86 @@
+import React, { useState, useEffect } from 'react'
+import classNames from 'classnames'
+import _ from 'lodash'
+import Provider from '../context'
+
+import './style'
+
+const prefixCls = 'hi-filter'
+
+const Filter = (props) => {
+ const { data = [], onChange, onClick, value, defaultValue = [], label = [], labelWith, showUnderline, theme } = props
+ const [options, setOptions] = useState(data || [])
+ const [selectId, setSelectId] = useState(value || defaultValue || [])
+ useEffect(() => {
+ setOptions(data)
+ }, [data])
+ useEffect(() => {
+ value && setSelectId(value)
+ }, [value])
+ const renderRadioItem = (item, levelActiveId, level) => {
+ const { id, content, disabled } = item
+ const active = item.id === levelActiveId
+ const cls = classNames(`${prefixCls}-content-item`, {
+ [`${prefixCls}-content-item__active`]: active,
+ [`${prefixCls}-content-item__disabled`]: disabled
+ })
+ return (
+ - {
+ if (disabled) {
+ return
+ }
+ const _activeId = selectId || []
+ _activeId.splice(level - 1)
+ _activeId[level - 1] = id
+ if (!value) {
+ setSelectId(_.cloneDeep(_activeId))
+ }
+ onClick && onClick(item)
+ onChange && !active && onChange(_activeId)
+ }}
+ >
+ {content}
+ {active && showUnderline && }
+
+ )
+ }
+ const renderCascadeContent = () => {
+ const content = []
+ let currentOptions = _.cloneDeep(options)
+ let level = 0
+ let levelLabel
+ while (currentOptions || levelLabel) {
+ const _currentOptions = _.cloneDeep(currentOptions)
+ currentOptions = false
+ const levelActiveId = selectId && selectId.length ? selectId[level] : null
+ const _label = label.length ? label[level] : undefined
+ level++
+ levelLabel = label.length ? label[level] : undefined
+ content.push(
+
+
+
+ {_label}
+
+
+
+ {_currentOptions &&
+ _currentOptions.map((item) => {
+ if (item.children && item.id === levelActiveId) {
+ currentOptions = item.children
+ }
+ return renderRadioItem(item, levelActiveId, level)
+ })}
+
+
+ )
+ }
+ return content
+ }
+
+ return {renderCascadeContent()}
+}
+export default Provider(Filter)
diff --git a/components/table/checkbox/style/index.js b/components/filter/style/index.js
similarity index 100%
rename from components/table/checkbox/style/index.js
rename to components/filter/style/index.js
diff --git a/components/filter/style/index.scss b/components/filter/style/index.scss
new file mode 100644
index 000000000..b34140856
--- /dev/null
+++ b/components/filter/style/index.scss
@@ -0,0 +1,74 @@
+@import '../../core-css/index.scss';
+$filter: 'hi-filter' !default;
+
+.#{$filter} {
+ &-content {
+ display: flex;
+
+ &-items {
+ display: flex;
+ flex-wrap: wrap;
+ margin: 0;
+ padding: 0;
+ padding-left: 16px;
+ }
+
+ &-item {
+ position: relative;
+ height: 40px;
+ font-size: $font-size-normal;
+ color: use-color('black');
+ line-height: 40px;
+ margin-right: 40px;
+ cursor: pointer;
+ transition: all 0.3s;
+ list-style: none;
+ }
+
+ &-item__active {
+ color: use-color('primary');
+ }
+
+ &-item__disabled {
+ cursor: not-allowed;
+ opacity: 0.4;
+ }
+
+ &-item__active--underline {
+ position: absolute;
+ width: 24px;
+ height: 2px;
+ display: inline-block;
+ background-color: use-color('primary');
+ bottom: 0;
+ left: 50%;
+ right: 50%;
+ transform: translate(-50%, -50%);
+ }
+
+ &-label {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ height: 40px;
+
+ &__title {
+ display: inline-block;
+ width: 76px;
+ font-size: $font-size-normal;
+ color: use-color('gray-70');
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+ }
+
+ &-label::after {
+ display: inline-block;
+ content: '';
+ width: 1px;
+ height: 14px;
+ background-color: #d8d8d8;
+ }
+ }
+}
diff --git a/components/form/Form.js b/components/form/Form.js
index a96056b0f..d7789aa63 100644
--- a/components/form/Form.js
+++ b/components/form/Form.js
@@ -1,110 +1,236 @@
-import React, { Component } from 'react'
+import React, { useEffect, useCallback, useReducer, forwardRef } from 'react'
+import _ from 'lodash'
import classNames from 'classnames'
import PropTypes from 'prop-types'
+import FormReducer, { FILEDS_UPDATE, FILEDS_UPDATE_LIST } from './FormReducer'
+import FormContext from './FormContext'
+import { transformValues } from './utils'
-class Form extends Component {
- constructor (props) {
- super(props)
+const getClassNames = (props) => {
+ const { labelPlacement, labelPosition, placement, inline, readOnly } = props
+ const _className = {}
- this.state = {
- fields: []
- }
+ if (labelPlacement || labelPosition) {
+ _className[`hi-form--label--${labelPlacement || labelPosition}`] = true
}
-
- getChildContext () {
- return {
- component: this
- }
- }
-
- getClassNames () {
- const { labelPlacement, labelPosition, placement, inline } = this.props
-
- const obj = {}
-
- if (labelPlacement || labelPosition) {
- obj[`hi-form--label--${labelPlacement || labelPosition}`] = true
- }
- if (placement === 'horizontal' || inline) {
- obj[`hi-form--inline`] = true
- }
-
- return obj
+ if (placement === 'horizontal' || inline) {
+ _className[`hi-form--inline`] = true
}
+ _className[`hi-form--readOnly`] = readOnly
+ return _className
+}
- addField (field) {
- this.setState((prevState) => ({
- fields: prevState.fields.concat(field)
- }))
- }
+const InternalForm = (props) => {
+ const {
+ children,
+ className,
+ style,
+ innerRef: formRef,
+ initialValues,
+ onValuesChange,
+ _type // SchemaForm 内部配置变量
+ } = props
+ const [state, dispatch] = useReducer(FormReducer, {
+ fields: [],
+ listNames: [],
+ listValues: {},
+ ...props
+ })
+ const { fields, listNames, listValues } = state
+ // 用户手动设置表单数据
+ const setFieldsValue = useCallback(
+ (values) => {
+ const _fields = _.cloneDeep(fields)
+ _fields.forEach((item) => {
+ const { field } = item
+ if (values.hasOwnProperty(field)) {
+ const value = values[field]
+ item.value = value
+ item.setValue(value)
+ }
+ })
+ dispatch({ type: FILEDS_UPDATE, payload: _fields })
+ // 处理 list value
+ Object.keys(values).forEach((key) => {
+ listNames.includes(key) &&
+ dispatch({
+ type: FILEDS_UPDATE_LIST,
+ payload: Object.assign({}, { ...listValues }, { [key]: values[key] })
+ })
+ })
+ },
+ [fields, listValues]
+ )
+ // 设置初始化的值
+ useEffect(() => {
+ // 处理 list value
+ initialValues &&
+ Object.keys(initialValues).forEach((key) => {
+ listNames.includes(key) &&
+ dispatch({
+ type: FILEDS_UPDATE_LIST,
+ payload: Object.assign({}, { ...listValues }, { [key]: initialValues[key] })
+ })
+ })
+ }, [])
+ // 转换值的输出
+ const internalValuesChange = useCallback(
+ (changeValues, allValues) => {
+ const _transformValues = transformValues(allValues, fields)
+ const _changeValues = _.cloneDeep(changeValues)
+
+ Object.keys(changeValues).forEach((changeValuesKey) => {
+ fields.forEach((filedItem) => {
+ const { field, _type, listname } = filedItem
+ if (field === changeValuesKey && _type === 'list') {
+ _changeValues[listname] = _transformValues[listname]
+ delete _changeValues[changeValuesKey]
+ }
+ })
+ })
- removeField (prop) {
- this.setState((prevState) => ({
- fields: prevState.fields.filter((field) => field.props.field !== prop)
- }))
- }
+ onValuesChange && onValuesChange(_changeValues, _transformValues)
+ },
+ [onValuesChange, fields]
+ )
+ // 重置校验
+ const resetValidates = useCallback(
+ (cb, resetNames, toDefault) => {
+ const changeValues = {}
+ const cacheallValues = {}
+ let _fields = _.cloneDeep(fields)
+ fields.forEach((item) => {
+ const { field, value } = item
+ cacheallValues[field] = value
+ })
- validate (cb) {
- let valid = true
- let count = 0
- const fields = this.state.fields
- if (fields.length === 0 && cb) {
- cb(valid)
- }
+ _fields = _fields.filter((childrenField) => {
+ return Array.isArray(resetNames) ? resetNames.includes(childrenField.field) : true
+ })
- fields.forEach((field) => {
- field.validate('', (errors) => {
- if (errors) {
- valid = false
- }
- if (typeof cb === 'function' && ++count === fields.length) {
- cb(valid)
+ _fields.forEach((childrenField) => {
+ const value =
+ toDefault && initialValues && initialValues[childrenField.field] ? initialValues[childrenField.field] : ''
+ if (!_.isEqual(childrenField.value, value)) {
+ changeValues[childrenField.field] = value
}
+
+ childrenField.value = value
+ childrenField.resetValidate(value)
})
- })
- }
- validateField (key, cb) {
- const field = this.state.fields.filter((field) => field.props.field === key)[0]
+ dispatch({ type: FILEDS_UPDATE, payload: _fields })
+ cb instanceof Function && cb()
+ // 比较耗性能
+ internalValuesChange(changeValues, Object.assign({}, { ...cacheallValues }, { ...changeValues }))
+ },
+ [fields, initialValues, onValuesChange]
+ )
+ // 对整个表单进行校验
+ const validate = useCallback(
+ (cb, validateNames) => {
+ const values = {}
+ let errors = {}
+
+ if (fields.length === 0 && cb) {
+ cb(values, errors)
+ return
+ }
+
+ const _fields = fields.filter((fieldChild) => {
+ const { field, value } = fieldChild
+ values[field] = value
+ return Array.isArray(validateNames) ? validateNames.includes(field) : true
+ })
- if (!field) {
- throw new Error('must call validate Field with valid key string!')
+ _fields.forEach((fieldChild) => {
+ const { field, value } = fieldChild
+ // 对指定的字段进行校验 其他字段过滤不校验
+ fieldChild.validate(
+ '',
+ (error) => {
+ if (error) {
+ const errorsMsg = error.map((err) => {
+ return err.message
+ })
+ errors[field] = { errors: errorsMsg }
+ }
+ },
+ value
+ )
+ })
+ errors = Object.keys(errors).length === 0 ? null : errors
+
+ cb && cb(transformValues(values, _fields), errors)
+ },
+ [fields]
+ )
+
+ const validateField = useCallback(
+ (key, cb) => {
+ let value
+ const field = fields.filter((fieldChild) => {
+ if (fieldChild.field === key) {
+ value = fieldChild.value
+ return true
+ }
+ })[0]
+
+ if (!field) {
+ throw new Error('must call validate Field with valid key string!')
+ }
+
+ field.validate(
+ '',
+ (error) => {
+ cb && cb(error)
+ },
+ value
+ )
+ },
+ [fields]
+ )
+
+ useEffect(() => {
+ if (!formRef) {
+ return
}
-
- field.validate('', cb)
- }
-
- resetValidates () {
- this.state.fields.forEach((field) => {
- field.resetValidate()
- })
- }
-
- render () {
- const { children, className, style } = this.props
-
- return (
-
- )
- }
+
+
+ )
}
-
-Form.childContextTypes = {
- component: PropTypes.any
-}
-
-Form.propTypes = {
- model: PropTypes.object,
+InternalForm.propTypes = {
rules: PropTypes.object,
labelPlacement: PropTypes.oneOf(['right', 'left', 'top']),
labelPosition: PropTypes.oneOf(['right', 'left', 'top']),
@@ -116,10 +242,12 @@ Form.propTypes = {
className: PropTypes.string,
style: PropTypes.object
}
-
-Form.defaultProps = {
+InternalForm.defaultProps = {
size: 'small',
showColon: true
}
+const Form = forwardRef((props, ref) => {
+ return
+})
export default Form
diff --git a/components/form/FormContext.js b/components/form/FormContext.js
new file mode 100644
index 000000000..5daac1068
--- /dev/null
+++ b/components/form/FormContext.js
@@ -0,0 +1,3 @@
+import React from 'react'
+const FormContext = React.createContext({})
+export default FormContext
diff --git a/components/form/FormReducer.js b/components/form/FormReducer.js
new file mode 100644
index 000000000..6544df5eb
--- /dev/null
+++ b/components/form/FormReducer.js
@@ -0,0 +1,34 @@
+export const FILEDS_INIT = 'FILEDS_INIT'
+export const FILEDS_UPDATE = 'FILEDS_UPDATE'
+export const FILEDS_UPDATE_VALUE = 'FILEDS_UPDATE_VALUE'
+export const FILEDS_REMOVE = 'FILEDS_REMOVE'
+export const FILEDS_INIT_LIST = 'FILEDS_INIT_LIST'
+export const FILEDS_UPDATE_LIST = 'FILEDS_UPDATE_LIST'
+const FormReducer = (state, action) => {
+ switch (action.type) {
+ case FILEDS_INIT:
+ const { fields } = state
+ const initfields = fields.filter((item) => {
+ return action.payload.field !== item.field
+ })
+ return Object.assign({}, { ...state }, { fields: initfields.concat(action.payload) })
+ case FILEDS_UPDATE:
+ return Object.assign({}, { ...state }, { fields: action.payload })
+ case FILEDS_REMOVE:
+ const _fields = state.fields.filter((item) => {
+ return action.payload !== item.field && action.payload !== item.propsField
+ })
+ return Object.assign({}, { ...state }, { fields: _fields })
+
+ case FILEDS_INIT_LIST:
+ const { listNames } = state
+ !listNames.includes(action.payload) && listNames.push(action.payload)
+
+ return Object.assign({}, { ...state }, { listNames: listNames })
+ case FILEDS_UPDATE_LIST:
+ return Object.assign({}, { ...state }, { listValues: action.payload })
+ default:
+ return state
+ }
+}
+export default FormReducer
diff --git a/components/form/Item.js b/components/form/Item.js
index a6eaf2c70..e8882de98 100644
--- a/components/form/Item.js
+++ b/components/form/Item.js
@@ -1,124 +1,204 @@
-import React, { Component } from 'react'
+import React, { useContext, useState, useEffect, useCallback, useRef } from 'react'
import classNames from 'classnames'
import AsyncValidator from 'async-validator'
import PropTypes from 'prop-types'
+import _ from 'lodash'
+
import { depreactedPropsCompat } from '../_util'
+import FormContext from './FormContext'
+import { FILEDS_INIT, FILEDS_UPDATE, FILEDS_REMOVE } from './FormReducer'
+import * as HIUI from '../'
-class FormItem extends Component {
- constructor (props, context) {
- super(props)
+// 指定子元素位置
+const getItemPosition = (itemPosition) => {
+ let _itemPosition = 'flex-end'
+ switch (itemPosition) {
+ case 'top':
+ _itemPosition = 'flex-start'
+ break
+ case 'center':
+ _itemPosition = 'center'
+ break
+ case 'bottom':
+ _itemPosition = 'flex-end'
+ break
+ default:
+ _itemPosition = 'center'
+ }
+ return _itemPosition
+}
- this.state = {
- error: '',
- valid: false,
- validating: false
- }
+const FormItem = (props) => {
+ const { formProps, formState, dispatch, internalValuesChange, listname, _type } = useContext(FormContext)
+ const {
+ children,
+ label,
+ required,
+ className,
+ showColon: shouldItemShowColon,
+ style,
+ field: propsField,
+ valuePropName = 'value',
+ contentPosition = 'center',
+ name,
+ listItemValue,
+ sort
+ } = props
- this.initValue = ''
+ const FormItemContent = useRef()
- this.parent = context.component
- }
+ const {
+ showColon: shouldFormShowColon,
+ initialValues = {},
+ localeDatas: {
+ form: { colon }
+ }
+ } = formProps || {}
+ // 初始化FormItem的内容
+ const { fields } = formState
+ const [value, setValue] = useState('')
+ const [error, setError] = useState('')
- componentDidMount () {
- const { field } = this.props
- if (field) {
- this.parent.addField(this)
- this.valueInit()
+ const getItemfield = useCallback(() => {
+ let _propsField = propsField
+ if (_type === 'list' && name) {
+ _propsField = _propsField + '#' + name
}
- }
+ return Array.isArray(propsField) ? propsField[propsField.length - 1] : _propsField
+ }, [propsField, name])
- componentWillUnmount () {
- this.parent.removeField(this.props.field)
- }
+ const [field, setField] = useState(getItemfield())
+ const [validating, setValidating] = useState(false)
+ useEffect(() => {
+ setField(getItemfield())
+ }, [propsField])
+ // 更新
+ const updateField = useCallback(
+ (_value, triggerType) => {
+ const childrenFiled = {
+ value: _value,
+ ...updateFieldInfoToReducer()
+ }
+ if (childrenFiled.field) {
+ const _fields = _.cloneDeep(fields)
- valueInit () {
- const value = this.parent.props.model[this.props.field]
- if (value === undefined) {
- this.initValue = value
- } else {
- this.initValue = JSON.parse(JSON.stringify(value))
- }
- }
+ _fields.forEach((item) => {
+ if (item.field === childrenFiled.field) {
+ Object.assign(item, childrenFiled)
+ }
+ })
+ const allValues = {}
+ _fields.forEach((item) => {
+ const { field, value } = item
+ allValues[field] = value
+ })
+ dispatch({ type: FILEDS_UPDATE, payload: _fields })
+ triggerType === 'onChange' && internalValuesChange({ [field]: _value }, allValues)
+ }
+ },
+ [fields]
+ )
- getRules () {
- let formRules = this.parent.props.rules
- let selfRules = this.props.rules
+ let rectCont = FormItemContent.current && FormItemContent.current.getBoundingClientRect()
- formRules = formRules ? formRules[this.props.field] : []
+ useEffect(() => {
+ rectCont = FormItemContent.current && FormItemContent.current.getBoundingClientRect()
+ }, [])
+ const resetValidate = useCallback((value = '') => {
+ // 清空数据
+ setValue(value)
+ setError('')
+ setValidating(false)
+ })
- return [].concat(selfRules || formRules || [])
- }
+ // 获取该单元的规则
+ const getRules = useCallback(() => {
+ const selfRules = required ? Object.assign({}, props.rules, { required }) : props.rules
+ let formRules = formProps.rules
- getFilteredRule (trigger) {
- const rules = this.getRules()
- return rules.filter(rule => {
+ formRules = formRules ? formRules[field] : []
+ return [].concat(selfRules || formRules || [])
+ }, [props, formProps, required])
+ // 过滤含有该trigger触发方式的rules
+ const getFilteredRule = useCallback((trigger) => {
+ const rules = getRules()
+ return rules.filter((rule) => {
return !rule.trigger || rule.trigger.indexOf(trigger) !== -1
})
- }
-
- getfieldValue () {
- const model = this.parent.props.model
- if (!model || !this.props.field) {
- return
- }
-
- const keyList = this.props.field.split(':')
- return keyList.length > 1 ? model[keyList[0]][keyList[1]] : model[this.props.field]
- }
-
- validate (trigger, cb) {
- const rules = this.getFilteredRule(trigger)
- if (!rules || rules.length === 0) {
+ })
+ // 父级调用
+ const validate = useCallback((trigger, cb, currentValue) => {
+ const triggerRules = getFilteredRule(trigger)
+ if (!triggerRules || triggerRules.length === 0) {
if (cb instanceof Function) {
cb()
}
-
return true
}
-
- this.setState({
- validating: true
- })
-
+ const rules = getRules()
const validator = new AsyncValidator({
- [this.props.field]: rules
+ [field]: rules
})
- const model = { [this.props.field]: this.getfieldValue() }
+ const model = { [field]: currentValue }
validator.validate(
model,
{
firstFields: true
},
- errors => {
- this.setState(
- {
- error: errors ? errors[0].message : '',
- validating: false,
- valid: !errors
- },
- () => {
- if (cb instanceof Function) {
- cb(errors)
- }
- }
- )
+ (errors) => {
+ setError(errors ? errors[0].message : '')
+ setValidating(false)
+ if (cb instanceof Function) {
+ cb(errors)
+ }
}
)
- }
+ })
- resetValidate () {
- this.setState({
- error: '',
- valid: true
- })
+ const updateFieldInfoToReducer = () => {
+ return {
+ field,
+ rules: getRules(),
+ resetValidate,
+ setValue,
+ validate,
+ propsField,
+ listname,
+ sort,
+ _type
+ }
}
+ // initValue
+ useEffect(() => {
+ const isExist = fields.some((item) => {
+ return item.field === field
+ })
+ if (field && !isExist) {
+ let value = initialValues && initialValues[field] ? initialValues[field] : ''
+ if (_type === 'list' && listItemValue) {
+ value = listItemValue[name] ? listItemValue[name] : listItemValue
+ }
+ dispatch({
+ type: FILEDS_INIT,
+ payload: {
+ value: value,
+ ...updateFieldInfoToReducer()
+ }
+ })
+ setValue(value)
+ }
+ return () => {
+ _type !== 'list' && dispatch({ type: FILEDS_REMOVE, payload: field })
+ }
+ }, [field])
- isRequired () {
- let rules = this.getRules()
+ // 判断是否含有Rules
+ const isRequired = useCallback(() => {
+ const rules = getRules()
let isRequired = false
if (rules && rules.length) {
- rules.every(rule => {
+ rules.every((rule) => {
if (rule.required) {
isRequired = true
return false
@@ -127,83 +207,139 @@ class FormItem extends Component {
})
}
return isRequired
- }
+ })
- handleFieldBlur () {
- const hasOnBlur = this.getRules().some(rule => (rule.trigger || '').includes('onBlur'))
- if (hasOnBlur) {
- this.validate('onBlur')
- }
- }
+ // 对字段的操作
+ const handleField = useCallback(
+ (triggerType, currentValue) => {
+ // 同步数据 reducer
+ updateField(currentValue, triggerType)
+ const rules = getRules()
+ const hasTriggerType = rules.some((rule) => {
+ const { trigger = '' } = rule
+ return trigger.includes(triggerType)
+ })
+ hasTriggerType && validate(triggerType, '', currentValue)
+ },
+ [fields]
+ )
+
+ const labelWidth = useCallback(() => {
+ const labelWidth = props.labelWidth || formProps.labelWidth
+ return formProps.labelPosition === 'top'
+ ? '100%'
+ : !Number.isNaN(Number(labelWidth))
+ ? Number(labelWidth)
+ : labelWidth
+ }, [props.labelWidth, formProps.labelWidth])
- handleFieldChange () {
- const hasOnChange = this.getRules().some(rule => (rule.trigger || '').includes('onChange'))
- if (hasOnChange) {
- this.validate('onChange')
+ const setEvent = (eventName, component, componentProps, e, ...args) => {
+ e.persist && e.persist()
+ const displayName = component && component.type && component.type.displayName
+
+ const _props = componentProps || children.props
+ eventName === 'onChange' && _props.onChange && _props.onChange(e, ...args)
+ eventName === 'onBlur' && _props.onBlur && _props.onBlur(e, ...args)
+ let value = e.target && e.target.hasOwnProperty(valuePropName) ? e.target[valuePropName] : e
+ if (displayName === 'Counter') {
+ value = args[0]
}
+ setValue(value)
+ handleField(eventName, value)
}
- get labelWidth () {
- const labelWidth = this.props.labelWidth || this.parent.props.labelWidth
+ // jsx渲染方式
+ const renderChildren = () => {
+ const { component, componentProps } = props
+
+ let _value = value
+ if (_type === 'list') {
+ const _fields = _.cloneDeep(fields)
+ _fields.forEach((item) => {
+ if (item.field === field) {
+ _value = item.value
+ }
+ })
+ }
+ // 对ScheamaForm表单Item进行特殊处理
+ if (_type === 'SchemaForm' && component) {
+ if (HIUI[component]) {
+ const HIUIComponent = HIUI[component]
+ return React.createElement(HIUIComponent, {
+ ...componentProps,
+ [valuePropName]: _value,
+ onChange: (e, ...args) => {
+ setEvent('onChange', HIUIComponent, componentProps, e, ...args)
+ },
+ onBlur: (e, ...args) => {
+ setEvent('onBlur', HIUIComponent, componentProps, e, ...args)
+ }
+ })
+ } else {
+ throw new Error('not found ' + component)
+ }
+ }
+ if (!children) {
+ return null
+ }
- return this.parent.props.labelPosition === 'top' ? false : labelWidth && parseInt(labelWidth)
+ return Array.isArray(children) || !React.isValidElement(children)
+ ? children
+ : React.cloneElement(children, {
+ [valuePropName]: _value,
+ onChange: (e, ...args) => {
+ setEvent('onChange', children, '', e, ...args)
+ },
+ onBlur: (e, ...args) => {
+ setEvent('onBlur', children, '', e, ...args)
+ }
+ })
}
- render () {
- const { children, label, required, className, showColon: shouldItemShowColon, style } = this.props
- const { showColon: shouldFormShowColon, localeDatas: {
- form: { colon }
- } } = this.parent.props
- const { error, validating } = this.state
- const shouldShowColon = shouldItemShowColon === undefined
- ? (shouldFormShowColon && typeof label === 'string' && label.trim())
+ const shouldShowColon =
+ shouldItemShowColon === undefined
+ ? shouldFormShowColon && typeof label === 'string' && label.trim()
: shouldItemShowColon
- const obj = {}
- obj['hi-form-item__error'] = error !== ''
- obj['hi-form-item--validating'] = validating
- obj['hi-form-item--required'] = this.isRequired() || required
-
- return (
-
- {
- (label || label === '') ? (
-
- ) : (
-
- )
- }
-
- {Array.isArray(children) || !children
- ? children
- : React.cloneElement(children, {
- onChange: (...args) => {
- children.props.onChange && children.props.onChange(...args)
- setTimeout(() => {
- this.handleFieldChange()
- })
- },
- onBlur: (...args) => {
- children.props.onBlur && children.props.onBlur(...args)
- setTimeout(() => {
- this.handleFieldBlur()
- })
- }
- })}
-
{error}
+ const obj = {}
+ obj['hi-form-item__error'] = error !== ''
+ obj['hi-form-item--validating'] = validating
+ obj['hi-form-item--required'] = isRequired() || required
+ const _labelWidth = labelWidth()
+ const contentWidth =
+ formProps.labelPosition !== 'top' && rectCont && rectCont.width ? rectCont.width - _labelWidth : '100%'
+ return (
+
+ {label || label === '' ? (
+
+ ) : (
+
+ )}
+
+
+ {renderChildren()}
+
+
+ {error}
- )
- }
-}
-
-FormItem.contextTypes = {
- component: PropTypes.any
+
+ )
}
FormItem.propTypes = {
- field: PropTypes.string,
+ field: PropTypes.oneOfType([PropTypes.string, PropTypes.array, PropTypes.number]),
rules: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
required: PropTypes.bool,
label: PropTypes.string,
@@ -216,5 +352,4 @@ FormItem.propTypes = {
FormItem.defaultProps = {
size: 'small'
}
-
export default depreactedPropsCompat([['field', 'prop']])(FormItem)
diff --git a/components/form/List.js b/components/form/List.js
new file mode 100644
index 000000000..fd402102f
--- /dev/null
+++ b/components/form/List.js
@@ -0,0 +1,68 @@
+import React, { useContext, useState, useEffect } from 'react'
+
+import FormContext from './FormContext'
+import { FILEDS_REMOVE, FILEDS_INIT_LIST, FILEDS_UPDATE } from './FormReducer'
+const List = (props) => {
+ const { dispatch, formState } = useContext(FormContext)
+ const { children, name } = props
+ const [listCount, setListCount] = useState([])
+ const { listValues } = formState
+
+ useEffect(() => {
+ // init listName
+ dispatch({ type: FILEDS_INIT_LIST, payload: name })
+ }, [])
+
+ // manage default value
+ useEffect(() => {
+ const cachelistCount = []
+ const { fields } = formState
+
+ const _fields = fields.filter((item) => {
+ return item.listname !== name
+ })
+ const values = listValues[name] ? listValues[name] : []
+ values.forEach((value, index) => {
+ const uuid = parseInt((Math.random() * 9 + 1) * 100000)
+ cachelistCount.push({
+ field: name + '-' + uuid,
+ listItemValue: value,
+ sort: index
+ })
+ })
+ dispatch({ type: FILEDS_UPDATE, payload: _fields })
+ setListCount(cachelistCount)
+ }, [listValues])
+
+ if (typeof children !== 'function') {
+ console.warning('Form.List only accepts function as children.')
+ return null
+ }
+
+ const add = () => {
+ const uuid = parseInt((Math.random() * 9 + 1) * 100000)
+ setListCount(
+ listCount.concat({
+ field: name + '-' + uuid,
+ sort: listCount.length
+ })
+ )
+ }
+
+ const remove = (fieldItem) => {
+ const _listCount = listCount.filter((item) => {
+ return item.field !== fieldItem.field
+ })
+ setListCount(_listCount)
+ dispatch({ type: FILEDS_REMOVE, payload: fieldItem.field })
+ }
+ return (
+
+
+ {children(listCount, { add, remove })}
+
+
+ )
+}
+
+export default List
diff --git a/components/form/Reset.js b/components/form/Reset.js
new file mode 100644
index 000000000..a7dba08dd
--- /dev/null
+++ b/components/form/Reset.js
@@ -0,0 +1,19 @@
+import React, { useContext } from 'react'
+
+import FormContext from './FormContext'
+
+import Button from '../button'
+
+const Reset = (props) => {
+ const { children = 'reset', onClick, fields: resetNames, toDefault = true } = props
+ const { resetValidates } = useContext(FormContext)
+ const reset = () => {
+ resetValidates(onClick, resetNames, toDefault)
+ }
+ return (
+
+ )
+}
+export default Reset
diff --git a/components/form/SchemaForm.js b/components/form/SchemaForm.js
new file mode 100644
index 000000000..7ec77b2f5
--- /dev/null
+++ b/components/form/SchemaForm.js
@@ -0,0 +1,63 @@
+import React, { useState, useEffect, useCallback, forwardRef } from 'react'
+import _ from 'lodash'
+import * as HIUI from '../'
+import Provider from '../context'
+import Form from './Form'
+import FormItem from './Item'
+import FormReset from './Reset'
+import FormSubmit from './Submit'
+
+const Group = {
+ 'Radio.Group': HIUI.Radio.Group,
+ 'Checkbox.Group': HIUI.Checkbox.Group
+}
+const prefixCls = 'hi-form-schema'
+
+const FormComponent = Provider(Form)
+
+const InternalSchemaForm = (props) => {
+ const { schema: schemaProps, children: childrenProps, submit, reset, innerRef } = props
+ const [schema, setSchema] = useState(schemaProps)
+ useEffect(() => {
+ setSchema(schemaProps)
+ }, [schemaProps])
+
+ const renderSchemaFormItem = useCallback(() => {
+ if (Array.isArray(schema)) {
+ return schema.map((schemaItem, index) => {
+ const { component, componentProps } = schemaItem
+ let child = null
+ if (HIUI[component] || Group[component]) {
+ const ChildComponent = HIUI[component] || Group[component]
+ child =
+ } else {
+ child =
{'not found ' + component}
+ }
+ return React.createElement(FormItem, {
+ ..._.omit(schemaItem, 'component', 'componentProps'),
+ key: component + index,
+ children: child
+ })
+ })
+ }
+ }, [schema])
+ return (
+
+
+ {renderSchemaFormItem()}
+ {childrenProps}
+ {(submit || reset) && (
+
+ {submit && }
+ {reset && }
+
+ )}
+
+
+ )
+}
+
+const SchemaForm = forwardRef((props, ref) => {
+ return
+})
+export default SchemaForm
diff --git a/components/form/Submit.js b/components/form/Submit.js
new file mode 100644
index 000000000..895e8e65e
--- /dev/null
+++ b/components/form/Submit.js
@@ -0,0 +1,18 @@
+import React, { useContext } from 'react'
+
+import FormContext from './FormContext'
+import Button from '../button'
+
+const Submit = (props) => {
+ const { children = 'submit', onClick, validate } = props
+ const { validate: formValidate } = useContext(FormContext)
+ const submit = () => {
+ onClick && formValidate(onClick, validate)
+ }
+ return (
+
+ )
+}
+export default Submit
diff --git a/components/form/__tests__/index.test.js b/components/form/__tests__/index.test.js
index 99fec346e..4d3821420 100644
--- a/components/form/__tests__/index.test.js
+++ b/components/form/__tests__/index.test.js
@@ -24,7 +24,7 @@ class Demo extends React.Component {
name: [
{
required: true,
- message:
请输入名称,
+ message:
请输入名称,
trigger: 'onBlur,onChange'
}
],
@@ -44,9 +44,9 @@ class Demo extends React.Component {
{
validator: (rule, value, cb) => {
const count = parseInt(value)
- if(isNaN(count)) {
+ if (isNaN(count)) {
cb('请输入数字')
- } else if(count <= 0) {
+ } else if (count <= 0) {
cb('必须是正数')
} else {
cb()
@@ -61,7 +61,7 @@ class Demo extends React.Component {
handleSubmit() {
this.form.current.validate(valid => {
- if(valid) {
+ if (valid) {
console.log(this.state.form)
alert('submit')
} else {
@@ -84,35 +84,44 @@ class Demo extends React.Component {
handleChange(key, e, value, index) {
this.setState({
- form: Object.assign({}, this.state.form, {[key]: value})
+ form: Object.assign({}, this.state.form, { [key]: value })
})
- if(index !== undefined) {
+ if (index !== undefined) {
this.setState({
checkedIndex: index
})
}
}
- render(){
+ render() {
const Row = Grid.Row
const Col = Grid.Col
- const {form, checkedIndex} = this.state
+ const { form, checkedIndex } = this.state
return (
-
@@ -137,7 +146,7 @@ class Demo2 extends React.Component {
name: [
{
required: true,
- message:
请输入名称,
+ message:
请输入名称,
trigger: 'onBlur,onChange'
}
],
@@ -157,9 +166,9 @@ class Demo2 extends React.Component {
{
validator: (rule, value, cb) => {
const count = parseInt(value)
- if(isNaN(count)) {
+ if (isNaN(count)) {
cb('请输入数字')
- } else if(count <= 0) {
+ } else if (count <= 0) {
cb('必须是正数')
} else {
cb()
@@ -174,7 +183,7 @@ class Demo2 extends React.Component {
handleSubmit() {
this.form.current.validate(valid => {
- if(valid) {
+ if (valid) {
console.log(this.state.form)
alert('submit')
} else {
@@ -184,21 +193,26 @@ class Demo2 extends React.Component {
})
}
-
- render(){
+ render() {
const Row = Grid.Row
const Col = Grid.Col
- const {form} = this.state
+ const { form } = this.state
return (
-
@@ -218,13 +232,13 @@ class Demo3 extends React.Component {
region: '',
count: ''
},
- checkedIndex: -1,
+ checkedIndex: -1
}
}
handleSubmit() {
this.form.current.validate(valid => {
- if(valid) {
+ if (valid) {
console.log(this.state.form)
alert('submit')
} else {
@@ -234,22 +248,23 @@ class Demo3 extends React.Component {
})
}
- render(){
+ render() {
const Row = Grid.Row
const Col = Grid.Col
- const {form} = this.state
+ const { form } = this.state
return (
-
@@ -261,11 +276,11 @@ describe('Form', () => {
it('should have the correct placement', () => {
const wrapper = mount(
+
-
-
+
+
)
@@ -281,22 +296,22 @@ describe('Form', () => {
;['left', 'right', 'top'].forEach(labelPlacement => {
const wrapper = mount(
+
-
-
+
+
)
expect(wrapper.find(`.hi-form--label--${labelPlacement}`)).toHaveLength(1)
const wrapperLegacy = mount(
+
-
-
+
+
)
@@ -309,16 +324,18 @@ describe('Form', () => {
it('should have the label width', () => {
const wrapper = mount(
+
-
-
+
+
)
expect(
- wrapper.find('label.hi-form-item__label').map(label => label.prop('style').width)
+ wrapper
+ .find('label.hi-form-item__label')
+ .map(label => label.prop('style').width)
).toEqual([50, 50])
wrapper.unmount()
})
@@ -326,41 +343,38 @@ describe('Form', () => {
it('should show colon', () => {
const wrapper = mount(
+
-
-
+
+
)
- expect(wrapper.find('label.hi-form-item__label').map(label => label.text())).toEqual([
- '账号:',
- '密码:'
- ])
+ expect(
+ wrapper.find('label.hi-form-item__label').map(label => label.text())
+ ).toEqual(['账号:', '密码:'])
wrapper.setProps({ showColon: false })
- expect(wrapper.find('label.hi-form-item__label').map(label => label.text())).toEqual([
- '账号',
- '密码'
- ])
+ expect(
+ wrapper.find('label.hi-form-item__label').map(label => label.text())
+ ).toEqual(['账号', '密码'])
wrapper.unmount()
})
it('should field has colon when field showColon is true and form showColon is false', () => {
const wrapper = mount(
+
-
-
+
+
)
- expect(wrapper.find('label.hi-form-item__label').map(label => label.text())).toEqual([
- '账号:',
- '密码'
- ])
+ expect(
+ wrapper.find('label.hi-form-item__label').map(label => label.text())
+ ).toEqual(['账号:', '密码'])
wrapper.unmount()
})
@@ -386,9 +400,9 @@ describe('Form', () => {
name: ''
}}
>
-
+
{
wrapper.setProps({ model: { name: e.target.value } })
}}
@@ -399,7 +413,9 @@ describe('Form', () => {
)
const cb = jest.fn()
wrapper.instance().validate(cb)
- expect(wrapper.find('.hi-form-item--msg__error').text()).toEqual('请输入名称')
+ expect(wrapper.find('.hi-form-item--msg__error').text()).toEqual(
+ '请输入名称'
+ )
wrapper.find('input').simulate('change', { target: { value: 'hiui' } })
jest.runAllTimers()
wrapper.instance().validate(cb)
@@ -409,7 +425,9 @@ describe('Form', () => {
wrapper.find('input').simulate('blur')
jest.runAllTimers()
expect(blurCb).toHaveBeenCalled()
- expect(wrapper.find('.hi-form-item--msg__error').text()).toEqual('请输入名称')
+ expect(wrapper.find('.hi-form-item--msg__error').text()).toEqual(
+ '请输入名称'
+ )
wrapper.unmount()
jest.useRealTimers()
@@ -436,9 +454,9 @@ describe('Form', () => {
name: ''
}}
>
-
+
{
wrapper.setProps({ model: { name: e.target.value } })
}}
@@ -449,7 +467,9 @@ describe('Form', () => {
)
const cb = jest.fn()
wrapper.instance().validateField('name', cb)
- expect(wrapper.find('.hi-form-item--msg__error').text()).toEqual('请输入名称')
+ expect(wrapper.find('.hi-form-item--msg__error').text()).toEqual(
+ '请输入名称'
+ )
wrapper.find('input').simulate('change', { target: { value: 'hiui' } })
jest.runAllTimers()
wrapper.instance().validate(cb)
@@ -459,33 +479,47 @@ describe('Form', () => {
jest.useRealTimers()
})
- it('cancelSubmit resetValidates',()=>{
- const wrapper = mount (
-
- )
- expect(wrapper.find('.hi-form')).toHaveLength(1)
- wrapper.find('button').at(0).simulate('click')
- expect(wrapper.find('.hi-form-item--msg__error').find('span')).toHaveLength(1)
- wrapper.find('button').at(1).simulate('click')
- expect(wrapper.find('.hi-form-item--msg__error').find('span')).toHaveLength(0)
- wrapper.unmount()
+ it('cancelSubmit resetValidates', () => {
+ const wrapper = mount()
+ expect(wrapper.find('.hi-form')).toHaveLength(1)
+ wrapper
+ .find('button')
+ .at(0)
+ .simulate('click')
+ expect(wrapper.find('.hi-form-item--msg__error').find('span')).toHaveLength(
+ 1
+ )
+ wrapper
+ .find('button')
+ .at(1)
+ .simulate('click')
+ expect(wrapper.find('.hi-form-item--msg__error').find('span')).toHaveLength(
+ 0
+ )
+ wrapper.unmount()
})
- it('validate When No item',()=>{
- const wrapper = mount (
-
- )
- expect(wrapper.find('.hi-form')).toHaveLength(1)
- wrapper.find('button').at(0).simulate('click')
- expect(wrapper.find('.hi-form-item--msg__error').find('span')).toHaveLength(0)
- wrapper.unmount()
+ it('validate When No item', () => {
+ const wrapper = mount()
+ expect(wrapper.find('.hi-form')).toHaveLength(1)
+ wrapper
+ .find('button')
+ .at(0)
+ .simulate('click')
+ expect(wrapper.find('.hi-form-item--msg__error').find('span')).toHaveLength(
+ 0
+ )
+ wrapper.unmount()
})
- it('validate When No rules',()=>{
- const wrapper = mount (
-
- )
- expect(wrapper.find('.hi-form')).toHaveLength(1)
- wrapper.find('button').at(0).simulate('click')
- expect(wrapper.find('.hi-form-item--msg__error').find('span')).toHaveLength(0)
- wrapper.unmount()
+ it('validate When No rules', () => {
+ const wrapper = mount()
+ expect(wrapper.find('.hi-form')).toHaveLength(1)
+ wrapper
+ .find('button')
+ .at(0)
+ .simulate('click')
+ expect(wrapper.find('.hi-form-item--msg__error').find('span')).toHaveLength(
+ 0
+ )
+ wrapper.unmount()
})
})
diff --git a/components/form/form-legacy/form-v2/Form.js b/components/form/form-legacy/form-v2/Form.js
new file mode 100644
index 000000000..f994fea68
--- /dev/null
+++ b/components/form/form-legacy/form-v2/Form.js
@@ -0,0 +1,117 @@
+import React, { Component } from 'react'
+import classNames from 'classnames'
+import PropTypes from 'prop-types'
+
+class Form extends Component {
+ constructor (props) {
+ super(props)
+
+ this.state = {
+ fields: []
+ }
+ }
+
+ getChildContext () {
+ return {
+ component: this
+ }
+ }
+
+ getClassNames () {
+ const { labelPlacement, labelPosition, placement, inline } = this.props
+
+ const obj = {}
+
+ if (labelPlacement || labelPosition) {
+ obj[`hi-form--legacy--label--${labelPlacement || labelPosition}`] = true
+ }
+ if (placement === 'horizontal' || inline) {
+ obj[`hi-form--legacy--inline`] = true
+ }
+
+ return obj
+ }
+
+ addField (field) {
+ this.setState((prevState) => ({
+ fields: prevState.fields.concat(field)
+ }))
+ }
+
+ removeField (prop) {
+ this.setState((prevState) => ({
+ fields: prevState.fields.filter((field) => field.props.field !== prop)
+ }))
+ }
+
+ validate (cb) {
+ let valid = true
+ let count = 0
+ const fields = this.state.fields
+ if (fields.length === 0 && cb) {
+ cb(valid)
+ }
+
+ fields.forEach((field) => {
+ field.validate('', (errors) => {
+ if (errors) {
+ valid = false
+ }
+ if (typeof cb === 'function' && ++count === fields.length) {
+ cb(valid)
+ }
+ })
+ })
+ }
+
+ validateField (key, cb) {
+ const field = this.state.fields.filter((field) => field.props.field === key)[0]
+
+ if (!field) {
+ throw new Error('must call validate Field with valid key string!')
+ }
+
+ field.validate('', cb)
+ }
+
+ resetValidates () {
+ this.state.fields.forEach((field) => {
+ field.resetValidate()
+ })
+ }
+
+ render () {
+ const { children, className, style } = this.props
+
+ return (
+
+ )
+ }
+}
+
+Form.childContextTypes = {
+ component: PropTypes.any
+}
+
+Form.propTypes = {
+ model: PropTypes.object,
+ rules: PropTypes.object,
+ labelPlacement: PropTypes.oneOf(['right', 'left', 'top']),
+ labelPosition: PropTypes.oneOf(['right', 'left', 'top']),
+ labWidth: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+ placement: PropTypes.oneOf(['horizontal', 'vertical']),
+ inline: PropTypes.bool,
+ onSubmit: PropTypes.func,
+ showColon: PropTypes.bool,
+ className: PropTypes.string,
+ style: PropTypes.object
+}
+
+Form.defaultProps = {
+ size: 'small',
+ showColon: true
+}
+
+export default Form
diff --git a/components/form/form-legacy/form-v2/Item.js b/components/form/form-legacy/form-v2/Item.js
new file mode 100644
index 000000000..bf842fb66
--- /dev/null
+++ b/components/form/form-legacy/form-v2/Item.js
@@ -0,0 +1,244 @@
+import React, { Component } from 'react'
+import classNames from 'classnames'
+import AsyncValidator from 'async-validator'
+import PropTypes from 'prop-types'
+import { depreactedPropsCompat } from '../../../_util'
+
+class FormItem extends Component {
+ constructor (props, context) {
+ super(props)
+
+ this.state = {
+ error: '',
+ valid: false,
+ validating: false
+ }
+
+ this.initValue = ''
+
+ this.parent = context.component
+ }
+
+ componentDidMount () {
+ const { field } = this.props
+ if (field) {
+ this.parent.addField(this)
+ this.valueInit()
+ }
+ }
+
+ componentWillUnmount () {
+ this.parent.removeField(this.props.field)
+ }
+
+ valueInit () {
+ const value = this.parent.props.model[this.props.field]
+ if (value === undefined) {
+ this.initValue = value
+ } else {
+ this.initValue = JSON.parse(JSON.stringify(value))
+ }
+ }
+
+ getRules () {
+ let formRules = this.parent.props.rules
+ let selfRules = this.props.rules
+
+ formRules = formRules ? formRules[this.props.field] : []
+
+ return [].concat(selfRules || formRules || [])
+ }
+
+ getFilteredRule (trigger) {
+ const rules = this.getRules()
+ return rules.filter(rule => {
+ return !rule.trigger || rule.trigger.indexOf(trigger) !== -1
+ })
+ }
+
+ getfieldValue () {
+ const model = this.parent.props.model
+ if (!model || !this.props.field) {
+ return
+ }
+
+ const keyList = this.props.field.split(':')
+ return keyList.length > 1
+ ? model[keyList[0]][keyList[1]]
+ : model[this.props.field]
+ }
+
+ validate (trigger, cb) {
+ const rules = this.getFilteredRule(trigger)
+ if (!rules || rules.length === 0) {
+ if (cb instanceof Function) {
+ cb()
+ }
+
+ return true
+ }
+
+ this.setState({
+ validating: true
+ })
+
+ const validator = new AsyncValidator({
+ [this.props.field]: rules
+ })
+ const model = { [this.props.field]: this.getfieldValue() }
+ validator.validate(
+ model,
+ {
+ firstFields: true
+ },
+ errors => {
+ this.setState(
+ {
+ error: errors ? errors[0].message : '',
+ validating: false,
+ valid: !errors
+ },
+ () => {
+ if (cb instanceof Function) {
+ cb(errors)
+ }
+ }
+ )
+ }
+ )
+ }
+
+ resetValidate () {
+ this.setState({
+ error: '',
+ valid: true
+ })
+ }
+
+ isRequired () {
+ let rules = this.getRules()
+ let isRequired = false
+
+ if (rules && rules.length) {
+ rules.every(rule => {
+ if (rule.required) {
+ isRequired = true
+ return false
+ }
+ return true
+ })
+ }
+ return isRequired
+ }
+
+ handleFieldBlur () {
+ const hasOnBlur = this.getRules().some(rule =>
+ (rule.trigger || '').includes('onBlur')
+ )
+ if (hasOnBlur) {
+ this.validate('onBlur')
+ }
+ }
+
+ handleFieldChange () {
+ const hasOnChange = this.getRules().some(rule =>
+ (rule.trigger || '').includes('onChange')
+ )
+ if (hasOnChange) {
+ this.validate('onChange')
+ }
+ }
+
+ get labelWidth () {
+ const labelWidth = this.props.labelWidth || this.parent.props.labelWidth
+
+ return this.parent.props.labelPosition === 'top'
+ ? false
+ : labelWidth && parseInt(labelWidth)
+ }
+
+ render () {
+ const {
+ children,
+ label,
+ required,
+ className,
+ showColon: shouldItemShowColon,
+ style
+ } = this.props
+ const {
+ showColon: shouldFormShowColon,
+ localeDatas: {
+ form: { colon }
+ }
+ } = this.parent.props
+ const { error, validating } = this.state
+ const shouldShowColon =
+ shouldItemShowColon === undefined
+ ? shouldFormShowColon && typeof label === 'string' && label.trim()
+ : shouldItemShowColon
+ const obj = {}
+ obj['hi-form-item--legacy__error'] = error !== ''
+ obj['hi-form-item--legacy--validating'] = validating
+ obj['hi-form-item--legacy--required'] = this.isRequired() || required
+
+ return (
+
+ {label || label === '' ? (
+
+ ) : (
+
+ )}
+
+ {Array.isArray(children) || !children
+ ? children
+ : React.cloneElement(children, {
+ onChange: (...args) => {
+ children.props.onChange && children.props.onChange(...args)
+ setTimeout(() => {
+ this.handleFieldChange()
+ })
+ },
+ onBlur: (...args) => {
+ children.props.onBlur && children.props.onBlur(...args)
+ setTimeout(() => {
+ this.handleFieldBlur()
+ })
+ }
+ })}
+
{error}
+
+
+ )
+ }
+}
+
+FormItem.contextTypes = {
+ component: PropTypes.any
+}
+
+FormItem.propTypes = {
+ field: PropTypes.string,
+ rules: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
+ required: PropTypes.bool,
+ label: PropTypes.string,
+ labelWidth: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+ showColon: PropTypes.bool,
+ className: PropTypes.string,
+ style: PropTypes.object
+}
+
+FormItem.defaultProps = {
+ size: 'small'
+}
+
+export default depreactedPropsCompat([['field', 'prop']])(FormItem)
diff --git a/components/form/form-legacy/form-v2/__tests__/index.test.js b/components/form/form-legacy/form-v2/__tests__/index.test.js
new file mode 100644
index 000000000..99fec346e
--- /dev/null
+++ b/components/form/form-legacy/form-v2/__tests__/index.test.js
@@ -0,0 +1,491 @@
+import React from 'react'
+import { mount } from 'enzyme'
+import Form from '../index'
+import Input from '../../input'
+import _Form from '../Form'
+import FormItem from '../Item'
+import Grid from '../../grid'
+import Button from '../../button'
+import Radio from '../../radio'
+class Demo extends React.Component {
+ constructor(props) {
+ super(props)
+
+ this.form = React.createRef()
+
+ this.state = {
+ form: {
+ name: '',
+ region: '',
+ count: ''
+ },
+ checkedIndex: -1,
+ rules: {
+ name: [
+ {
+ required: true,
+ message: 请输入名称,
+ trigger: 'onBlur,onChange'
+ }
+ ],
+ region: [
+ {
+ required: true,
+ message: '请选择区域',
+ trigger: 'onChange'
+ }
+ ],
+ count: [
+ {
+ required: true,
+ message: '请输入数量',
+ trigger: 'onChange'
+ },
+ {
+ validator: (rule, value, cb) => {
+ const count = parseInt(value)
+ if(isNaN(count)) {
+ cb('请输入数字')
+ } else if(count <= 0) {
+ cb('必须是正数')
+ } else {
+ cb()
+ }
+ },
+ trigger: 'onChange'
+ }
+ ]
+ }
+ }
+ }
+
+ handleSubmit() {
+ this.form.current.validate(valid => {
+ if(valid) {
+ console.log(this.state.form)
+ alert('submit')
+ } else {
+ console.log('error')
+ return false
+ }
+ })
+ }
+
+ cancelSubmit() {
+ this.setState({
+ form: {
+ name: '',
+ region: '',
+ count: ''
+ }
+ })
+ this.form.current.resetValidates()
+ }
+
+ handleChange(key, e, value, index) {
+ this.setState({
+ form: Object.assign({}, this.state.form, {[key]: value})
+ })
+
+ if(index !== undefined) {
+ this.setState({
+ checkedIndex: index
+ })
+ }
+ }
+
+ render(){
+ const Row = Grid.Row
+ const Col = Grid.Col
+ const {form, checkedIndex} = this.state
+
+ return (
+
+
+
+ )
+ }
+}
+class Demo2 extends React.Component {
+ constructor(props) {
+ super(props)
+
+ this.form = React.createRef()
+
+ this.state = {
+ form: {
+ name: '',
+ region: '',
+ count: ''
+ },
+ checkedIndex: -1,
+ rules: {
+ name: [
+ {
+ required: true,
+ message: 请输入名称,
+ trigger: 'onBlur,onChange'
+ }
+ ],
+ region: [
+ {
+ required: true,
+ message: '请选择区域',
+ trigger: 'onChange'
+ }
+ ],
+ count: [
+ {
+ required: true,
+ message: '请输入数量',
+ trigger: 'onChange'
+ },
+ {
+ validator: (rule, value, cb) => {
+ const count = parseInt(value)
+ if(isNaN(count)) {
+ cb('请输入数字')
+ } else if(count <= 0) {
+ cb('必须是正数')
+ } else {
+ cb()
+ }
+ },
+ trigger: 'onChange'
+ }
+ ]
+ }
+ }
+ }
+
+ handleSubmit() {
+ this.form.current.validate(valid => {
+ if(valid) {
+ console.log(this.state.form)
+ alert('submit')
+ } else {
+ console.log('error')
+ return false
+ }
+ })
+ }
+
+
+ render(){
+ const Row = Grid.Row
+ const Col = Grid.Col
+ const {form} = this.state
+
+ return (
+
+
+
+ )
+ }
+}
+class Demo3 extends React.Component {
+ constructor(props) {
+ super(props)
+
+ this.form = React.createRef()
+
+ this.state = {
+ form: {
+ name: '',
+ region: '',
+ count: ''
+ },
+ checkedIndex: -1,
+ }
+ }
+
+ handleSubmit() {
+ this.form.current.validate(valid => {
+ if(valid) {
+ console.log(this.state.form)
+ alert('submit')
+ } else {
+ console.log('error')
+ return false
+ }
+ })
+ }
+
+ render(){
+ const Row = Grid.Row
+ const Col = Grid.Col
+ const {form} = this.state
+ return (
+
+
+
+ )
+ }
+}
+describe('Form', () => {
+ it('should have the correct placement', () => {
+ const wrapper = mount(
+
+
+
+
+
+
+
+ )
+ expect(wrapper.find('.hi-form--line')).toHaveLength(0)
+ wrapper.setProps({ placement: 'vertical' })
+ expect(wrapper.find('.hi-form--line')).toHaveLength(0)
+ wrapper.setProps({ placement: 'horizontal' })
+ expect(wrapper.find('.hi-form--inline')).toHaveLength(1)
+ wrapper.unmount()
+ })
+
+ it('should align the label by labelPlacement', () => {
+ ;['left', 'right', 'top'].forEach(labelPlacement => {
+ const wrapper = mount(
+
+
+
+
+
+
+
+ )
+ expect(wrapper.find(`.hi-form--label--${labelPlacement}`)).toHaveLength(1)
+ const wrapperLegacy = mount(
+
+
+
+
+
+
+
+ )
+ // expect(wrapperLegacy.find(`.hi-form--label--${labelPlacement}`)).toHaveLength(1)
+ wrapper.unmount()
+ wrapperLegacy.unmount()
+ })
+ })
+
+ it('should have the label width', () => {
+ const wrapper = mount(
+
+
+
+
+
+
+
+ )
+ expect(
+ wrapper.find('label.hi-form-item__label').map(label => label.prop('style').width)
+ ).toEqual([50, 50])
+ wrapper.unmount()
+ })
+
+ it('should show colon', () => {
+ const wrapper = mount(
+
+
+
+
+
+
+
+ )
+ expect(wrapper.find('label.hi-form-item__label').map(label => label.text())).toEqual([
+ '账号:',
+ '密码:'
+ ])
+ wrapper.setProps({ showColon: false })
+ expect(wrapper.find('label.hi-form-item__label').map(label => label.text())).toEqual([
+ '账号',
+ '密码'
+ ])
+ wrapper.unmount()
+ })
+
+ it('should field has colon when field showColon is true and form showColon is false', () => {
+ const wrapper = mount(
+
+
+
+
+
+
+
+ )
+ expect(wrapper.find('label.hi-form-item__label').map(label => label.text())).toEqual([
+ '账号:',
+ '密码'
+ ])
+ wrapper.unmount()
+ })
+
+ it('should validate require field', () => {
+ jest.useFakeTimers()
+ const localeDatas = {
+ form: {
+ colon: ':'
+ }
+ }
+ const blurCb = jest.fn()
+ const wrapper = mount(
+ <_Form
+ rules={{
+ name: {
+ required: true,
+ message: '请输入名称',
+ trigger: 'onChange'
+ }
+ }}
+ localeDatas={localeDatas}
+ model={{
+ name: ''
+ }}
+ >
+
+ {
+ wrapper.setProps({ model: { name: e.target.value } })
+ }}
+ onBlur={blurCb}
+ />
+
+
+ )
+ const cb = jest.fn()
+ wrapper.instance().validate(cb)
+ expect(wrapper.find('.hi-form-item--msg__error').text()).toEqual('请输入名称')
+ wrapper.find('input').simulate('change', { target: { value: 'hiui' } })
+ jest.runAllTimers()
+ wrapper.instance().validate(cb)
+ expect(wrapper.find('.hi-form-item--msg__error').text()).toEqual('')
+ wrapper.find('input').simulate('change', { target: { value: '' } })
+ jest.runAllTimers()
+ wrapper.find('input').simulate('blur')
+ jest.runAllTimers()
+ expect(blurCb).toHaveBeenCalled()
+ expect(wrapper.find('.hi-form-item--msg__error').text()).toEqual('请输入名称')
+ wrapper.unmount()
+
+ jest.useRealTimers()
+ })
+
+ it('should validate single field', () => {
+ jest.useFakeTimers()
+ const localeDatas = {
+ form: {
+ colon: ':'
+ }
+ }
+ const wrapper = mount(
+ <_Form
+ rules={{
+ name: {
+ required: true,
+ message: '请输入名称',
+ trigger: 'onChange'
+ }
+ }}
+ localeDatas={localeDatas}
+ model={{
+ name: ''
+ }}
+ >
+
+ {
+ wrapper.setProps({ model: { name: e.target.value } })
+ }}
+ onBlur={jest.fn()}
+ />
+
+
+ )
+ const cb = jest.fn()
+ wrapper.instance().validateField('name', cb)
+ expect(wrapper.find('.hi-form-item--msg__error').text()).toEqual('请输入名称')
+ wrapper.find('input').simulate('change', { target: { value: 'hiui' } })
+ jest.runAllTimers()
+ wrapper.instance().validate(cb)
+ expect(wrapper.find('.hi-form-item--msg__error').text()).toEqual('')
+ expect(() => wrapper.instance().validateField('errorField', cb)).toThrow()
+ wrapper.unmount()
+
+ jest.useRealTimers()
+ })
+ it('cancelSubmit resetValidates',()=>{
+ const wrapper = mount (
+
+ )
+ expect(wrapper.find('.hi-form')).toHaveLength(1)
+ wrapper.find('button').at(0).simulate('click')
+ expect(wrapper.find('.hi-form-item--msg__error').find('span')).toHaveLength(1)
+ wrapper.find('button').at(1).simulate('click')
+ expect(wrapper.find('.hi-form-item--msg__error').find('span')).toHaveLength(0)
+ wrapper.unmount()
+ })
+ it('validate When No item',()=>{
+ const wrapper = mount (
+
+ )
+ expect(wrapper.find('.hi-form')).toHaveLength(1)
+ wrapper.find('button').at(0).simulate('click')
+ expect(wrapper.find('.hi-form-item--msg__error').find('span')).toHaveLength(0)
+ wrapper.unmount()
+ })
+ it('validate When No rules',()=>{
+ const wrapper = mount (
+
+ )
+ expect(wrapper.find('.hi-form')).toHaveLength(1)
+ wrapper.find('button').at(0).simulate('click')
+ expect(wrapper.find('.hi-form-item--msg__error').find('span')).toHaveLength(0)
+ wrapper.unmount()
+ })
+})
diff --git a/components/form/form-legacy/form-v2/index.js b/components/form/form-legacy/form-v2/index.js
new file mode 100644
index 000000000..2b39a8b97
--- /dev/null
+++ b/components/form/form-legacy/form-v2/index.js
@@ -0,0 +1,9 @@
+import Form from './Form'
+import Item from './Item'
+import Provider from '../../../context'
+import './style/index'
+const FormWrapper = Provider(Form)
+FormWrapper.Item = Item
+
+export { Item }
+export default FormWrapper
diff --git a/components/tree/tree-legacy/style/index.js b/components/form/form-legacy/form-v2/style/index.js
similarity index 100%
rename from components/tree/tree-legacy/style/index.js
rename to components/form/form-legacy/form-v2/style/index.js
diff --git a/components/form/form-legacy/form-v2/style/index.scss b/components/form/form-legacy/form-v2/style/index.scss
new file mode 100644
index 000000000..70cc3a8f1
--- /dev/null
+++ b/components/form/form-legacy/form-v2/style/index.scss
@@ -0,0 +1,138 @@
+@import '@hi-ui/core-css/index.scss';
+
+.hi-form--legacy {
+ max-width: 100%;
+ position: relative;
+
+ fieldset {
+ margin: 0;
+ padding: 0;
+ border: 0;
+
+ & + fieldset {
+ margin-top: 16px;
+ }
+ }
+
+ legend {
+ margin: 0 0 16px;
+ color: $gray-darker;
+ }
+
+ &--inline {
+ display: flex;
+ flex-wrap: wrap;
+ }
+
+ &__title {
+ box-sizing: border-box;
+ margin-top: 0;
+ margin-bottom: 24px;
+ font-size: 16px;
+ font-weight: 400;
+ }
+
+ &--label--left {
+ .hi-form-item--legacy__label {
+ text-align: left;
+ }
+ }
+
+ &--label--right {
+ .hi-form-item--legacy__label {
+ text-align: right;
+ }
+ }
+
+ &--label--top {
+ padding-right: 0;
+ text-align: left;
+
+ &.hi-form--inline {
+ // 上下布局只在水平表单中自动填充高度
+ .hi-form-item--legacy__span {
+ min-height: 32px;
+ }
+ }
+
+ .hi-form-item--legacy {
+ flex-direction: column;
+
+ .hi-form-item--legacy__label {
+ height: 22px;
+ padding-right: 8px;
+ margin-bottom: 8px;
+ }
+ }
+ }
+
+ .hi-form-item--legacy {
+ display: flex;
+ font-size: $font-size-normal;
+ margin-right: $spacer-2;
+
+ &__label,
+ &__span {
+ flex-shrink: 0;
+ box-sizing: border-box;
+ vertical-align: top;
+ color: $gray-darker;
+ overflow-wrap: break-word;
+ }
+
+ &__label {
+ min-height: 32px;
+ padding-right: $spacer-2;
+ line-height: 32px;
+ }
+
+ &__content {
+ flex: 1;
+ position: relative;
+ vertical-align: top;
+ min-height: 32px;
+ }
+
+ &__error {
+ .hi-form-item--legacy__content {
+ .hi-input__inner,
+ .hi-select__input,
+ .hi-cascader__input-container,
+ .hi-datepicker__input,
+ .tree-select__tag-wrapper,
+ .hi-input {
+ border-color: get-color($palette-secondary, 'danger');
+ }
+ }
+ }
+
+ &--msg__error {
+ font-size: $font-size-small;
+ min-height: 24px;
+ padding: 2px 0;
+ box-sizing: border-box;
+ line-height: 20px;
+ color: get-color($palette-secondary, 'danger');
+
+ .hi-form--inline & {
+ top: 36px;
+ left: 0;
+ line-height: 18px;
+ }
+ }
+
+ &--required {
+ .hi-form-item--legacy__label::before {
+ margin-right: $spacer-1;
+ content: '*';
+ color: get-color($palette-secondary, 'danger');
+ }
+ }
+
+ &--fixed {
+ position: absolute;
+ top: 0;
+ right: 0;
+ }
+ }
+}
diff --git a/components/form/hooks/useForm.js b/components/form/hooks/useForm.js
new file mode 100644
index 000000000..c3e47437f
--- /dev/null
+++ b/components/form/hooks/useForm.js
@@ -0,0 +1,40 @@
+import { useCallback } from 'react'
+import useFormInstance from './useFormInstance'
+
+const useForm = () => {
+ const { FormWrapper, FormInstance: formRef, FormItem } = useFormInstance()
+ const { current } = formRef
+ const validate = useCallback(
+ (cb, validate) => {
+ const { validate: formValidate } = formRef.current
+ formValidate(cb, validate)
+ },
+ [current]
+ )
+ const resetValidates = useCallback(
+ (cb, resetNames, toDefault) => {
+ const { resetValidates } = formRef.current
+
+ resetValidates(cb, resetNames, toDefault)
+ },
+ [current]
+ )
+ const setFieldsValue = useCallback(
+ values => {
+ formRef.current.setFieldsValue(values)
+ },
+ [current]
+ )
+ const validateField = useCallback(
+ (field, cb) => {
+ formRef.current.validateField(field, cb)
+ },
+ [current]
+ )
+ return {
+ FormWrapper,
+ FormItem,
+ FormInstance: { validate, setFieldsValue, resetValidates, validateField }
+ }
+}
+export default useForm
diff --git a/components/form/hooks/useFormInstance.js b/components/form/hooks/useFormInstance.js
new file mode 100644
index 000000000..783061630
--- /dev/null
+++ b/components/form/hooks/useFormInstance.js
@@ -0,0 +1,17 @@
+import React, { useRef, forwardRef } from 'react'
+import Provider from '../../context'
+import Form from '../Form'
+import FormItem from '../Item'
+import FormReset from '../Reset'
+import FormSubmit from '../Submit'
+const FormComponent = Provider(Form)
+
+const useFormInstance = () => {
+ const formRef = useRef()
+ let FormInstance = formRef
+ const FormWrapper = forwardRef((props, ref) => {
+ return
+ })
+ return { FormWrapper, FormInstance, FormItem, FormReset, FormSubmit }
+}
+export default useFormInstance
diff --git a/components/form/index.d.ts b/components/form/index.d.ts
new file mode 100644
index 000000000..ae1041e96
--- /dev/null
+++ b/components/form/index.d.ts
@@ -0,0 +1,54 @@
+import { CSSProperties } from "react"
+
+type formData = {
+ [prop: string]: any
+}
+interface FormProps {
+ initialValues?: formData
+ rules?: object
+ labelWidth?: string | number
+ labelPlacement?: 'right' | 'left' | 'top'
+ placement?: 'horizontal' | 'vertical'
+ showColon?: boolean
+ children: Form.Item
+}
+
+interface ItemProps {
+ field?: string
+ label?: string
+ labelWidth?: string
+ required?: boolean
+ showColon?: boolean
+}
+interface SchemaItem extends ItemProps {
+ component?: string
+ componentProps?: string
+}
+interface SchemaProps {
+ schema?: SchemaItem
+ submit?: FormSubmit
+}
+
+interface FormSubmit extends ButtonProps{
+ onClick?: (value: object, errors: object) => void
+ validate?: any[]
+}
+interface FormReset extends ButtonProps{
+ onClick?: () => void
+ fields?: any[]
+ toDefault?: boolean
+}
+interface FormList {
+ name?: string
+}
+declare class Item extends React.Component {
+}
+declare class SchemaForm extends React.Component {
+}
+declare class Form extends React.Component {
+ static Item = Item
+ static SchemaForm = SchemaForm
+ static FormReset = FormReset
+}
+
+export default Form
diff --git a/components/form/index.js b/components/form/index.js
index ccdc24296..6ec479600 100644
--- a/components/form/index.js
+++ b/components/form/index.js
@@ -1,8 +1,25 @@
-import Form from './Form'
+import LegacyForm, { Item as ItemV2 } from './form-legacy/form-v2'
+import FormV3 from './Form'
import Item from './Item'
+import Submit from './Submit'
+import Reset from './Reset'
+import List from './List'
+import SchemaForm from './SchemaForm'
+import useForm from './hooks/useForm'
+
import Provider from '../context'
import './style/index'
-const FormWrapper = Provider(Form)
-FormWrapper.Item = Item
-export default FormWrapper
+LegacyForm.Item = ItemV2
+
+const Form = Provider(FormV3)
+
+Form.Item = Item
+Form.Submit = Submit
+Form.Reset = Reset
+Form.List = List
+Form.SchemaForm = SchemaForm
+Form.useForm = useForm
+
+export default Form
+export { LegacyForm }
diff --git a/components/form/style/index.scss b/components/form/style/index.scss
index ded932442..4a277389b 100644
--- a/components/form/style/index.scss
+++ b/components/form/style/index.scss
@@ -1,4 +1,4 @@
-@import '@hi-ui/core-css/index.scss';
+@import '../../core-css/index.scss';
.hi-form {
max-width: 100%;
@@ -16,7 +16,7 @@
legend {
margin: 0 0 16px;
- color: $gray-darker;
+ color: use-color('gray-80');
}
&--inline {
@@ -24,12 +24,16 @@
flex-wrap: wrap;
}
+ &--readOnly {
+ pointer-events: none;
+ }
+
&__title {
box-sizing: border-box;
margin-top: 0;
margin-bottom: 24px;
- font-size: 16px;
- font-weight: 400;
+ font-size: $font-size-large;
+ font-weight: $font-weight-normal;
}
&--label--left {
@@ -77,7 +81,7 @@
flex-shrink: 0;
box-sizing: border-box;
vertical-align: top;
- color: $gray-darker;
+ color: use-color('gray-80');
overflow-wrap: break-word;
}
@@ -92,6 +96,17 @@
position: relative;
vertical-align: top;
min-height: 32px;
+ flex-shrink: 0;
+
+ p {
+ margin: 0;
+ padding: 0;
+ }
+ }
+
+ &__children {
+ display: flex;
+ min-height: 30px;
}
&__error {
@@ -113,8 +128,16 @@
padding: 2px 0;
box-sizing: border-box;
line-height: 20px;
+ transition: all 0.3s;
+ transform: translateY(-10%);
+ opacity: 0;
color: get-color($palette-secondary, 'danger');
+ &__show {
+ opacity: 1;
+ transform: translateY(0);
+ }
+
.hi-form--inline & {
top: 36px;
left: 0;
@@ -135,4 +158,8 @@
top: 0;
right: 0;
}
+
+ .hi-checkbox-group {
+ line-height: 32px;
+ }
}
diff --git a/components/form/utils.js b/components/form/utils.js
new file mode 100644
index 000000000..9cfefa0ad
--- /dev/null
+++ b/components/form/utils.js
@@ -0,0 +1,53 @@
+import _ from 'lodash'
+
+// 需要做一个filed的规则解析
+const tranformListValues = (field, listNestValues, value, listname) => {
+ const key = field.split('#')[1]
+ const keyName = field.split('#')[0]
+ if (listNestValues[listname][keyName]) {
+ listNestValues[listname][keyName] = _.merge(listNestValues[listname][keyName], {
+ [key]: value
+ })
+ } else {
+ listNestValues[listname][keyName] = { [key]: value }
+ }
+}
+// 转换输出的值
+export const transformValues = (allvalue, fields) => {
+ let tranformValues = {}
+ const listNestValues = {}
+ // 根据sort进行数据排列
+ const sortfields = _.sortBy(fields, ['sort'])
+ sortfields.forEach((filedItem) => {
+ const { field, propsField, _type, listname } = filedItem
+ if (_type === 'list') {
+ if (propsField !== field) {
+ listNestValues[listname] = listNestValues[listname] || {}
+ tranformListValues(field, listNestValues, allvalue[field], listname)
+ Object.keys(listNestValues).forEach((key) => {
+ const arr = []
+ Object.keys(listNestValues[key]).forEach((item) => {
+ arr.push(listNestValues[key][item])
+ })
+ tranformValues[key] = arr
+ })
+ return
+ }
+ if (tranformValues[listname]) {
+ tranformValues[listname].push(allvalue[field])
+ } else {
+ tranformValues[listname] = [allvalue[field]]
+ }
+ } else {
+ if (Array.isArray(propsField)) {
+ const chainKeys = propsField.reduceRight((pre, next) => {
+ return { [next]: pre }
+ }, allvalue[field])
+ tranformValues = _.merge(tranformValues, chainKeys)
+ } else {
+ tranformValues = _.merge(tranformValues, { [field]: allvalue[field] })
+ }
+ }
+ })
+ return tranformValues
+}
diff --git a/components/grid/index.d.ts b/components/grid/index.d.ts
new file mode 100644
index 000000000..20a75d297
--- /dev/null
+++ b/components/grid/index.d.ts
@@ -0,0 +1,22 @@
+import { CSSProperties } from "react"
+
+interface GridProps {
+}
+interface RowProps {
+ justify?: 'flex-start' | 'flex-end' | 'center' | 'space-around' | 'space-between '
+ gutter?: boolean
+}
+interface ColProps {
+ justify?: 'flex-start' | 'flex-end' | 'center' | 'space-around' | 'space-between '
+ span?: number
+ offset?: number
+}
+declare class Row extends React.Component {
+}
+declare class Col extends React.Component {
+}
+declare class Grid extends React.Component {
+ static Row = Row
+ static Col = Col
+}
+export default Grid
diff --git a/components/grid/style/index.scss b/components/grid/style/index.scss
index 8e6f03293..46ecbadaa 100644
--- a/components/grid/style/index.scss
+++ b/components/grid/style/index.scss
@@ -1,4 +1,4 @@
-@import '@hi-ui/core-css/index.scss';
+@import '../../core-css/index.scss';
// Rows
.hi-grid__row {
diff --git a/components/icon/iconfont.js b/components/icon/iconfont.js
new file mode 100644
index 000000000..4fb4b4ead
--- /dev/null
+++ b/components/icon/iconfont.js
@@ -0,0 +1,59 @@
+!(function (a) {
+ var l
+ var h
+ var o
+ var v
+ var i
+ var t
+ var d =
+ ''
+ var m = (m = document.getElementsByTagName('script'))[m.length - 1].getAttribute('data-injectcss')
+ if (m && !a.__iconfont__svg__cssinject__) {
+ a.__iconfont__svg__cssinject__ = !0
+ try {
+ document.write(
+ ''
+ )
+ } catch (a) {
+ console && console.log(a)
+ }
+ }
+ function c() {
+ i || ((i = !0), o())
+ }
+ ;(l = function () {
+ var a, l, h, o
+ ;((o = document.createElement('div')).innerHTML = d),
+ (d = null),
+ (h = o.getElementsByTagName('svg')[0]) &&
+ (h.setAttribute('aria-hidden', 'true'),
+ (h.style.position = 'absolute'),
+ (h.style.width = 0),
+ (h.style.height = 0),
+ (h.style.overflow = 'hidden'),
+ (a = h),
+ (l = document.body).firstChild ? ((o = a), (h = l.firstChild).parentNode.insertBefore(o, h)) : l.appendChild(a))
+ }),
+ document.addEventListener
+ ? ~['complete', 'loaded', 'interactive'].indexOf(document.readyState)
+ ? setTimeout(l, 0)
+ : ((h = function () {
+ document.removeEventListener('DOMContentLoaded', h, !1), l()
+ }),
+ document.addEventListener('DOMContentLoaded', h, !1))
+ : document.attachEvent &&
+ ((o = l),
+ (v = a.document),
+ (i = !1),
+ (t = function () {
+ try {
+ v.documentElement.doScroll('left')
+ } catch (a) {
+ return void setTimeout(t, 50)
+ }
+ c()
+ })(),
+ (v.onreadystatechange = function () {
+ v.readyState == 'complete' && ((v.onreadystatechange = null), c())
+ }))
+})(window)
diff --git a/components/icon/index.js b/components/icon/index.js
index 1c0d96cf5..4dc158c78 100644
--- a/components/icon/index.js
+++ b/components/icon/index.js
@@ -1,15 +1,73 @@
import React from 'react'
import classNames from 'classnames'
-import './style/index'
-
-class Icon extends React.Component {
- render () {
- const {className, name, style, ...rest} = this.props
- const iconCls = classNames(`hi-icon`, `icon-${name}`, className)
+import './style/index'
+import './iconfont.js'
- return
- }
+// 旧版本 icon name 兼容映射
+const legacyMap = {
+ top: 'to-top',
+ bottom: 'to-bottom',
+ 'check-circle-o': 'check-circle',
+ 'close-circle-o': 'close-circle',
+ ratio: 'equal-proportion',
+ stattistics: 'pie-chart',
+ freezing: 'freeze-column',
+ caveat: 'warning',
+ noapi: 'show-code',
+ api: 'close-code',
+ web: 'webpage',
+ voice: 'audio',
+ usergroup: 'users',
+ 'upload-cloud': 'cloud-upload',
+ telephone: 'phone',
+ store: 'shop',
+ set: 'setting',
+ repeat: 'time-rewind',
+ qr: 'qr-code',
+ plugin: 'duplicate',
+ prompt: 'bell',
+ process: 'diagram',
+ 'problem-circle-o': 'question-circle',
+ position: 'location',
+ pic: 'picture',
+ pc: 'monitor',
+ move: 'drag',
+ 'move-to': 'folder-move',
+ more: 'ellipsis',
+ 'more-circle-o': 'ellipsis-circle',
+ mark: 'pin',
+ 'mail-delivery': 'mail-send',
+ linechart: 'bar-chart',
+ label: 'tag',
+ internet: 'global',
+ info: 'document-exclamation',
+ 'info-circle-o': 'info-circle',
+ hide: 'eye-invisible',
+ component: 'relation',
+ data: 'data-monitor',
+ 'money-circle-o': 'rmb',
+ refer: 'document-search'
}
+const Icon = ({ name, filled = false, className, style = {}, onClick }) => {
+ return (
+
+ )
+}
export default Icon
diff --git a/components/icon/style/index.js b/components/icon/style/index.js
index 522460139..63810a681 100644
--- a/components/icon/style/index.js
+++ b/components/icon/style/index.js
@@ -1 +1 @@
-import '../../style/icon/index.scss'
+import './index.scss'
diff --git a/components/icon/style/index.scss b/components/icon/style/index.scss
new file mode 100644
index 000000000..d985b903c
--- /dev/null
+++ b/components/icon/style/index.scss
@@ -0,0 +1,7 @@
+svg.hi-icon {
+ width: 1em;
+ height: 1em;
+ vertical-align: -0.15em !important;
+ fill: currentColor;
+ overflow: hidden;
+}
diff --git a/components/index.d.ts b/components/index.d.ts
new file mode 100644
index 000000000..0a6e5c841
--- /dev/null
+++ b/components/index.d.ts
@@ -0,0 +1,41 @@
+export { default as Button } from './button'
+export { default as Card } from './card/index'
+export { default as Checkbox } from './checkbox'
+export { default as Carousel } from './carousel'
+export { default as Cascader } from './cascader'
+export { default as Collapse } from './collapse'
+export { default as Counter } from './counter'
+export { default as Datepicker } from './date-picker'
+export { default as TimePicker } from './date-picker/TimePicker'
+export { default as Dropdown } from './dropdown'
+export { default as Form } from './form'
+export { default as Grid } from './grid'
+export { default as Input } from './input'
+export { default as Loading } from './loading'
+export { default as Menu } from './menu'
+export { default as Message } from './message'
+export { default as Modal } from './modal'
+export { default as Notification } from './notification'
+export { default as Pagination } from './pagination'
+export { default as Popover } from './popover'
+export { default as Progress } from './progress'
+export { default as Radio } from './radio'
+export { default as Rate } from './rate'
+export { default as Select } from './select'
+export { default as Stepper } from './stepper'
+export { default as Switch } from './switch'
+export { default as Table } from './table'
+export { default as Tabs } from './tabs'
+export { default as Tag } from './tag'
+export { default as Timeline } from './timeline'
+export { default as Tooltip } from './tooltip'
+export { default as Transfer } from './transfer'
+export { default as Tree } from './tree'
+export { default as Upload } from './upload'
+export { default as Watermark } from './watermark'
+export { default as List } from './list'
+export { default as SelectTree } from './select-tree'
+export { default as Filter } from './filter'
+export { default as Drawer } from './drawer'
+export { default as Search } from './search'
+export { default as Slider } from './slider'
diff --git a/components/index.js b/components/index.js
index c870cc54e..dc8657442 100755
--- a/components/index.js
+++ b/components/index.js
@@ -3,13 +3,15 @@ export { default as Dropdown } from './dropdown'
export { default as Pagination } from './pagination'
export { default as Tabs } from './tabs'
export { default as Table } from './table'
+export { LegacyTable } from './table'
export { default as Notification } from './notification'
export { default as handleNotificate } from './notification/HandleNotification'
export { default as Modal } from './modal'
+export { default as Drawer } from './drawer'
export { default as Alert } from './alert'
-export { default as Panel } from './panel'
export { default as Collapse } from './collapse'
export { default as Select } from './select'
+export { default as SelectTree } from './select-tree'
export { default as Cascader } from './cascader'
export { default as Tooltip } from './tooltip'
export { default as Popover } from './popover'
@@ -19,17 +21,18 @@ export { default as Upload } from './upload'
export { default as Tree } from './tree'
export { default as Input } from './input'
export { default as DatePicker } from './date-picker'
-export { default as TimePicker } from './date-picker/TimePicker'
+export { TimePicker } from './date-picker'
export { default as Counter } from './counter'
-export { default as NavMenu } from './nav-menu'
export { default as Badge } from './badge'
export { default as Radio } from './radio'
export { default as Checkbox } from './checkbox'
export { default as Grid } from './grid'
export { default as Layout } from './grid'
export { default as Stepper } from './stepper'
-export { default as confirm } from './confirm'
+
export { default as Form } from './form'
+export { LegacyForm } from './form'
+
export { default as Menu } from './menu'
export { default as FormItem } from './form/Item'
export { default as Ficon } from './ficon'
@@ -45,4 +48,7 @@ export { default as Tag } from './tag'
export { default as Breadcrumb } from './breadcrumb'
export { default as Carousel } from './carousel'
export { default as Watermark } from './watermark'
+export { default as Slider } from './slider'
+export { default as Search } from './search'
export { ThemeContext, LocaleContext } from './context'
+export { default as HiRequest } from './_util/hi-request'
diff --git a/components/input/Input.js b/components/input/Input.js
index 5685c625b..61b524654 100644
--- a/components/input/Input.js
+++ b/components/input/Input.js
@@ -7,8 +7,7 @@ import { format, formatValue, getAttrs, formatAmount, filterObjProps } from './u
* @prop type 输入框类型
*/
class Input extends Component {
-
- constructor (props) {
+ constructor(props) {
super(props)
this._Input = React.createRef()
@@ -44,7 +43,7 @@ class Input extends Component {
}
}
- componentWillReceiveProps (nextProps) {
+ componentWillReceiveProps(nextProps) {
if (nextProps.value !== undefined) {
if (nextProps.value !== this.state.value) {
this.setState({
@@ -67,9 +66,11 @@ class Input extends Component {
})
}
}
+
focus = () => {
this._Input.current.focus()
}
+
blur = () => {
this._Input.current.blur()
}
@@ -77,10 +78,10 @@ class Input extends Component {
/**
* 渲染 text 输入框
*/
- renderText () {
+ renderText() {
let { hover, active, value } = this.state
// clearableTrigger 为内部预留,主要表示清除按钮的触发形态,类型分为 'hover' 和 ‘always’
- let { disabled, type, id, placeholder, clearable, clearableTrigger = 'hover' } = this.props
+ let { disabled, type, id, placeholder, clearable, clearableTrigger = 'hover',localeDatas } = this.props
let { prefix, suffix, prepend, append } = this.state
const noClear = ['textarea']
let prefixId = id ? id + '_prefix' : ''
@@ -234,7 +235,7 @@ class Input extends Component {
/**
* 渲染 textarea
*/
- renderTextarea () {
+ renderTextarea() {
let { active } = this.state
let { disabled, theme } = this.props
const { defaultValue, ...attrs } = this.attrs
@@ -316,7 +317,7 @@ class Input extends Component {
)
}
- render () {
+ render() {
const { type } = this.attrs
const { size, id, className, required, theme } = this.props
diff --git a/components/input/index.d.ts b/components/input/index.d.ts
new file mode 100644
index 000000000..53b639d9b
--- /dev/null
+++ b/components/input/index.d.ts
@@ -0,0 +1,19 @@
+interface Props {
+ type?: 'text' | 'textarea' | 'id' | 'tel' | 'card' | 'amount' | 'email'
+ value?: string | number
+ defaultValue?: string | number
+ prepend?: string | JSX.Element
+ append?: string | JSX.Element
+ disabled?: boolean
+ clearable?: boolean
+ placeholder?: string
+ onFocus?: (e: React.FocusEvent) => void
+ onBlur?: (e: React.FocusEvent) => void
+ onKeyDown?: (e: React.KeyboardEvent) => void
+ onKeyPress?: (e: React.KeyboardEvent) => void
+ onKeyUp?: (e: React.KeyboardEvent) => void
+ onInput?: (e: React.FormEvent) => void
+ onChange?: (e: React.ChangeEvent) => void
+}
+declare const Input: React.ComponentType
+export default Input
diff --git a/components/input/index.js b/components/input/index.js
index 3a3275926..23c1c1e7a 100644
--- a/components/input/index.js
+++ b/components/input/index.js
@@ -5,7 +5,8 @@ import './style/index'
import InputLegacy from './input-legacy'
import Provider from '../context'
-function SwitchVersion (component = {}, componentLegacy = {}) {
+function SwitchVersion(component = {}, componentLegacy = {}) {
+ // eslint-disable-next-line react/display-name
return forwardRef((props, ref) => {
const InnerComponent = props.legacy === true ? componentLegacy : component
return
@@ -14,4 +15,4 @@ function SwitchVersion (component = {}, componentLegacy = {}) {
export default SwitchVersion(Provider(Input), InputLegacy)
-export {Input}
+export { Input }
diff --git a/components/input/style/index.scss b/components/input/style/index.scss
index 85a504eea..496e27762 100644
--- a/components/input/style/index.scss
+++ b/components/input/style/index.scss
@@ -1,5 +1,4 @@
-@import './variables.scss';
-@import '@hi-ui/core-css/index.scss';
+@import '../../core-css/index.scss';
$input: 'hi-input' !default;
@@ -7,47 +6,47 @@ textarea.#{$input} {
display: block;
box-shadow: none;
outline: none;
- color: $normal-color;
- font-size: $font-size;
+ color: use-color('black');
+ font-size: $font-size-normal;
min-height: 32px;
height: auto;
- line-height: $line-height;
+ line-height: $line-height-size-normal;
resize: vertical;
overflow: auto;
padding: 4px 12px;
- border: 1px solid $border-color-normal;
+ border: 1px solid use-color('gray-30');
box-sizing: border-box;
- border-radius: $border-radius;
- background-color: #fff;
+ border-radius: 2px;
+ background-color: use-color('white');
transition: border-color 0.3s;
&:not(.disabled):hover {
- border-color: $border-color-active;
+ border-color: use-color('primary');
}
&.active {
- border-color: $border-color-active;
+ border-color: use-color('primary');
}
&.disabled {
- color: $disabled-color;
- background-color: $disabled-bg-color;
+ color: use-color('gray-50');
+ background-color: use-color('gray-10');
}
&::-webkit-input-placeholder {
- color: $placeholder-color;
+ color: use-color('gray-50');
}
&:-moz-placeholder {
- color: $placeholder-color;
+ color: use-color('gray-50');
}
&::-moz-placeholder {
- color: $placeholder-color;
+ color: use-color('gray-50');
}
&:-ms-input-placeholder {
- color: $placeholder-color;
+ color: use-color('gray-50');
}
}
@@ -60,19 +59,19 @@ textarea.#{$input} {
box-sizing: border-box;
::-webkit-input-placeholder {
- color: $placeholder-color;
+ color: use-color('gray-50');
}
:-moz-placeholder {
- color: $placeholder-color;
+ color: use-color('gray-50');
}
::-moz-placeholder {
- color: $placeholder-color;
+ color: use-color('gray-50');
}
:-ms-input-placeholder {
- color: $placeholder-color;
+ color: use-color('gray-50');
}
input {
@@ -81,14 +80,14 @@ textarea.#{$input} {
outline: none;
min-height: 30px;
box-sizing: border-box;
- color: $normal-color;
- font-size: $font-size;
- line-height: $line-height;
+ color: use-color('black');
+ font-size: $font-size-normal;
+ line-height: $line-height-size-normal;
box-shadow: none;
&.disabled {
- color: $disabled-color;
- background-color: $disabled-bg-color;
+ color: use-color('gray-50');
+ background-color: use-color('gray-10');
}
}
@@ -112,23 +111,23 @@ textarea.#{$input} {
position: relative;
display: flex;
width: 100%;
- border: 1px solid $border-color-normal;
+ border: 1px solid use-color('gray-30');
box-sizing: border-box;
- border-radius: $border-radius;
- background-color: #fff;
+ border-radius: 2px;
+ background-color: use-color('white');
transition: border-color 0.3s;
&:not(.disabled):hover {
- border-color: $border-color-active;
+ border-color: use-color('primary');
}
&.active {
- border-color: $border-color-active;
+ border-color: use-color('primary');
}
&.disabled {
- color: $disabled-color;
- background-color: $disabled-bg-color;
+ color: use-color('gray-50');
+ background-color: use-color('gray-10');
}
}
@@ -150,9 +149,13 @@ textarea.#{$input} {
.hi-input__clear {
display: block;
border-radius: 50%;
- color: #666;
- font-size: 14px;
+ color: use-color('gray-50');
+ font-size: 22px;
text-align: center;
+
+ &:hover {
+ color: use-color('black');
+ }
}
.hi-input__suffix__icon {
@@ -177,8 +180,8 @@ textarea.#{$input} {
flex-direction: column;
justify-content: space-around;
flex: 1 0 auto;
- color: $normal-color;
- font-size: $font-size;
+ color: use-color('black');
+ font-size: $font-size-normal;
line-height: 30px;
}
@@ -216,7 +219,7 @@ textarea.#{$input} {
}
&__append {
- color: $border-color-active;
+ color: use-color('primary');
.hi-btn,
.hi-select__input {
@@ -239,35 +242,7 @@ textarea.#{$input} {
display: flex;
justify-content: space-around;
align-items: center;
- color: #f00;
- }
- }
-}
-
-@each $key, $value in $palette-primary {
- .theme__#{$key}.#{$input} {
- .hi-input__inner:not(.disabled):hover {
- border-color: $value;
- }
-
- .hi-input__inner.active {
- border-color: $value;
- }
-
- .hi-input--append {
- .hi-input__append {
- color: $value;
- }
- }
- }
-
- textarea.#{$input}.theme__#{$key} {
- &:not(.disabled):hover {
- border-color: $value;
- }
-
- &.active {
- border-color: $value;
+ color: get-color($palette-secondary, 'danger');
}
}
}
diff --git a/components/input/style/variables.scss b/components/input/style/variables.scss
deleted file mode 100644
index d681a0377..000000000
--- a/components/input/style/variables.scss
+++ /dev/null
@@ -1,39 +0,0 @@
-/* stylelint-disable */
-// color
-$normal-color: #333;
-
-$border-color-normal: #d8d8d8;
-$border-color-active: #4284f5;
-$border-radius: 2px;
-
-$disabled-bg-color: #f2f2f2;
-$disabled-color: #ccc;
-
-$placeholder-color: $disabled-color;
-
-$label-bg-color: #eee;
-
-
-// size m
-$font-size: 14px;
-$line-height: 22px;
-$placeholder-size: $font-size;
-
-$m-font-size: 14px;
-$m-placeholder-size: $font-size;
-
-$m-width: 240px;
-
-
-// size s
-$s-font-size: 12px;
-$s-placeholder-size: 12px;
-
-$s-width: 200px;
-
-
-// size l
-$l-font-size: 16px;
-$l-placeholder-size: 16px;
-
-$l-width: 280px;
diff --git a/components/input/util.js b/components/input/util.js
index f31bd23cb..59f4af0d8 100644
--- a/components/input/util.js
+++ b/components/input/util.js
@@ -1,13 +1,13 @@
/**
- * 提取非函数属性
- * @param {object} oldProps 原始props
- */
+ * 提取非函数属性
+ * @param {object} oldProps 原始props
+ */
export const getAttrs = (oldProps) => {
const noNeed = ['value', 'className', 'class', 'id', 'style', 'size', 'disabled']
const attrs = {}
const events = {}
- for (let i in oldProps) {
+ for (const i in oldProps) {
if (!(oldProps[i] instanceof Function)) {
if (noNeed.indexOf(i) === -1) {
attrs[i] = oldProps[i]
@@ -76,7 +76,17 @@ export const formatCard = (val) => {
} else if (len < 17) {
return val.slice(0, 4) + ' ' + val.slice(4, 8) + ' ' + val.slice(8, 12) + ' ' + val.slice(12, 16)
} else {
- return val.slice(0, 4) + ' ' + val.slice(4, 8) + ' ' + val.slice(8, 12) + ' ' + val.slice(12, 16) + ' ' + val.slice(16, 19)
+ return (
+ val.slice(0, 4) +
+ ' ' +
+ val.slice(4, 8) +
+ ' ' +
+ val.slice(8, 12) +
+ ' ' +
+ val.slice(12, 16) +
+ ' ' +
+ val.slice(16, 19)
+ )
}
}
@@ -120,12 +130,16 @@ export const formatValue = (val, type) => {
const numberType = ['id', 'tel', 'amount', 'card']
if (numberType.indexOf(type) > -1) {
- let tmp = val.replace(/[^\d|.]/g, '')
+ const tmp = val.replace(/[^\d|.]/g, '')
switch (type) {
- case 'id': return tmp.slice(0, 18)
- case 'tel': return tmp.slice(0, 11)
- case 'card': return tmp.slice(0, 19)
- default: return tmp
+ case 'id':
+ return tmp.slice(0, 18)
+ case 'tel':
+ return tmp.slice(0, 11)
+ case 'card':
+ return tmp.slice(0, 19)
+ default:
+ return tmp
}
} else {
return val
@@ -136,8 +150,10 @@ export const formatValue = (val, type) => {
* 过滤属性
*/
export const filterObjProps = (obj, propsNeedFilter) => {
- return Object.keys(obj).filter(key => !propsNeedFilter.includes(key)).reduce((filteredObj, key) => {
- filteredObj[key] = obj[key]
- return filteredObj
- }, {})
+ return Object.keys(obj)
+ .filter((key) => !propsNeedFilter.includes(key))
+ .reduce((filteredObj, key) => {
+ filteredObj[key] = obj[key]
+ return filteredObj
+ }, {})
}
diff --git a/components/list/IconText.js b/components/list/IconText.js
new file mode 100644
index 000000000..e1cdd28f2
--- /dev/null
+++ b/components/list/IconText.js
@@ -0,0 +1,20 @@
+import React from 'react'
+import Icon from '../Icon'
+
+import classNames from 'classnames'
+
+import './style'
+const prefixCls = 'hi-icontext'
+const IconText = ({ name, style = {}, iconStyle, className, text }) => {
+ return (
+
+
+ {text}
+
+ )
+}
+export default IconText
diff --git a/components/list/Item.js b/components/list/Item.js
new file mode 100644
index 000000000..f2d564465
--- /dev/null
+++ b/components/list/Item.js
@@ -0,0 +1,56 @@
+import React from 'react'
+import classNames from 'classnames'
+
+const prefixCls = 'hi-list'
+const isArray = arg => {
+ return arg instanceof Array
+}
+const ExtraArray = ({ extra }) => {
+ return (
+
+ {extra.map((item, index) => {
+ return (
+ -
+ {item}
+
+ )
+ })}
+
+ )
+}
+const Item = ({ title, titleTag, titleTagType, description, extra }) => {
+ return (
+
+ {(title || titleTag) && (
+
+ {title && (
+
+ {title}
+
+ )}
+ {titleTag && (
+
+ {titleTag}
+
+ )}
+
+ )}
+ {description && (
+
+ {description}
+
+ )}
+ {extra && (
+
+ {isArray(extra) ? : extra}
+
+ )}
+
+ )
+}
+export default Item
diff --git a/components/list/index.d.ts b/components/list/index.d.ts
new file mode 100644
index 000000000..a0091d43d
--- /dev/null
+++ b/components/list/index.d.ts
@@ -0,0 +1,23 @@
+type DataItem = {
+ title: string | JSX.Element
+ titleTag?: string | JSX.Element
+ titleTagType?: 'primary' | 'success' |'warning' | 'danger'
+ description?: string | JSX.Element
+ extra?: string | string[]
+ avatar?: string
+}
+interface Props {
+ type?: 'default' | 'card'
+ data: DataItem[]
+ renderItem?: (item: DataItem) => JSX.Element
+ action?: (item: DataItem) => JSX.Element
+ actionPosition?: 'top' | 'center' |'bottom'
+ split?: boolean
+ pagination?: boolean | PaginationProps
+ bordered?: boolean
+ hoverable?: boolean
+ layout?: 'vertical' | 'horizontal'
+ emptyText?: string | JSX.Element
+}
+declare const List: React.ComponentType
+export default List
diff --git a/components/list/index.js b/components/list/index.js
new file mode 100644
index 000000000..fe2c94647
--- /dev/null
+++ b/components/list/index.js
@@ -0,0 +1,130 @@
+import React, { useState, useEffect } from 'react'
+import classNames from 'classnames'
+
+import Pagination from '../pagination'
+import Item from './Item'
+import IconText from './IconText'
+import './style'
+
+const prefixCls = 'hi-list'
+
+const getPagePosition = pagination => {
+ let pagePosition = 'flex-end'
+ switch (pagination.position) {
+ case 'left':
+ pagePosition = 'start'
+ break
+ case 'middle':
+ pagePosition = 'center'
+ break
+
+ case 'right':
+ pagePosition = 'flex-end'
+ break
+ default:
+ pagePosition = 'flex-end'
+ }
+ return pagePosition
+}
+const getActionPosition = actionPosition => {
+ let _actionPosition = 'flex-end'
+ switch (actionPosition) {
+ case 'top':
+ _actionPosition = 'flex-start'
+ break
+ case 'center':
+ _actionPosition = 'center'
+ break
+ case 'bottom':
+ _actionPosition = 'flex-end'
+ break
+ default:
+ _actionPosition = 'flex-end'
+ }
+ return _actionPosition
+}
+const List = ({
+ data,
+ bordered,
+ type,
+ layout,
+ hoverable,
+ emptyText = '暂无数据',
+ pagination = {},
+ action,
+ actionPosition = '',
+ renderItem,
+ split = true,
+ ...others
+}) => {
+ const [datas, setData] = useState(data)
+ const paginationProps = pagination
+ useEffect(() => {
+ setData(data)
+ }, [data])
+ const renderListItem = (item, index) => {
+ const { avatar } = item || {}
+
+ return (
+ -
+
+ {avatar && (
+
+
+
+ )}
+ {renderItem && renderItem(item)}
+
+
+ {action && (
+
+ {action(item)}
+
+ )}
+
+ )
+ }
+ return (
+
+
+ {datas && datas.length ? (
+ datas.map((item, index) => {
+ return renderListItem(item, index)
+ })
+ ) : (
+ - {emptyText}
+ )}
+
+ {paginationProps && (
+
+ )}
+
+ )
+}
+List.Item = Item
+export { IconText }
+export default List
diff --git a/components/list/style/index.js b/components/list/style/index.js
new file mode 100644
index 000000000..63810a681
--- /dev/null
+++ b/components/list/style/index.js
@@ -0,0 +1 @@
+import './index.scss'
diff --git a/components/list/style/index.scss b/components/list/style/index.scss
new file mode 100644
index 000000000..f1dce4383
--- /dev/null
+++ b/components/list/style/index.scss
@@ -0,0 +1,159 @@
+@import '../../core-css/index.scss';
+
+$list: 'hi-list' !default;
+
+$color-map: (
+ 'primary': use-color('primary'),
+ 'warning': get-color($palette-secondary, 'warning'),
+ 'success': get-color($palette-secondary, 'success'),
+ 'danger': get-color($palette-secondary, 'danger')
+) !default;
+
+.#{$list} {
+ &-items {
+ margin: 0;
+ padding: 0;
+
+ &__border {
+ border: 1px solid use-color('gray-30');
+ border-bottom: none;
+
+ .hi-list-item {
+ padding-left: 12px;
+ }
+ }
+ }
+
+ &-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 20px 24px 16px 0;
+
+ &__row {
+ display: flex;
+ align-items: flex-start;
+ }
+
+ &-title__content {
+ display: flex;
+ align-items: center;
+ }
+
+ &__title {
+ padding: 0;
+ margin: 0;
+ color: use-color('black');
+ font-size: $font-size-large;
+ line-height: 24px;
+ font-weight: $font-weight-normal;
+ }
+
+ &__titleTag {
+ height: 18px;
+ font-size: $font-size-small;
+ font-weight: $font-weight-normal;
+ line-height: 18px;
+ margin-left: 8px;
+ padding: 0 6px;
+ border-radius: 9px;
+ color: use-color('primary');
+ border: 1px solid use-color('primary');
+ }
+
+ &__desc {
+ color: use-color('black');
+ font-size: $font-size-small;
+ line-height: 20px;
+ }
+
+ &__extra {
+ color: use-color('gray-70');
+ font-size: $font-size-small;
+ line-height: 20px;
+ }
+
+ &__avatar {
+ margin-right: 14px;
+
+ img {
+ width: 72px;
+ height: 72px;
+ display: inline-block;
+ }
+ }
+
+ &__action {
+ color: use-color('black');
+ font-size: $font-size-small;
+ line-height: 20px;
+ text-align: center;
+ cursor: pointer;
+ }
+
+ &__split {
+ border-bottom: 1px solid use-color('gray-30');
+ }
+ }
+
+ &-item__card {
+ border: 1px solid use-color('gray-30');
+ margin-top: 12px;
+ padding: 8px 16px 8px 12px;
+ }
+
+ &-item__colume {
+ flex-direction: column;
+ align-items: flex-start;
+ }
+
+ &-item--empty {
+ color: use-color('gray-70');
+ text-align: center;
+ line-height: 88px;
+ list-style: none;
+ }
+
+ &-item__hoverable:hover {
+ box-shadow: 2px 2px 5px 2px rgba(0, 0, 0, 0.08);
+ }
+
+ &-extra-items {
+ display: flex;
+ align-items: center;
+ margin: 0;
+ padding: 0;
+
+ li {
+ list-style: none;
+ }
+ }
+
+ &-extra-item {
+ line-height: 20px;
+
+ &::after {
+ display: inline-block;
+ content: '';
+ width: 1px;
+ height: 10px;
+ margin: 0 12px;
+ background-color: use-color('gray-30');
+ }
+
+ &:last-child {
+ &::after {
+ display: inline-block;
+ content: '';
+ width: 0;
+ }
+ }
+ }
+
+ @each $key, $value in $color-map {
+ &-titleTag__#{$key} {
+ color: $value;
+ border: 1px solid $value;
+ }
+ }
+}
diff --git a/components/loading/index.d.ts b/components/loading/index.d.ts
new file mode 100644
index 000000000..c6a993c3e
--- /dev/null
+++ b/components/loading/index.d.ts
@@ -0,0 +1,21 @@
+interface Props {
+ size?: 'large' | 'default' | 'small'
+ content?: string | JSX.Element
+ visible?: boolean
+ full?: boolean
+}
+
+type Options = {
+ content?: string | JSX.Element
+ key: string | number
+ duration?: number
+ size?: 'large' | 'default' | 'small'
+}
+const OpenFun: (target: HTMLElement, options: Options) => void
+const CloseFun: (key: string | number) => void
+
+declare class Loading extends React.Component {
+ static open = OpenFun
+ static close = CloseFun
+}
+export default Loading
diff --git a/components/loading/style/index.scss b/components/loading/style/index.scss
index 1c287b0bb..a767deeea 100755
--- a/components/loading/style/index.scss
+++ b/components/loading/style/index.scss
@@ -126,7 +126,6 @@
background: #4284f5;
transform: translateX(100%);
animation: animDotL 1.5s linear infinite;
- // display: none;
}
}
}
diff --git a/components/locales/en-US.js b/components/locales/en-US.js
index ca765265b..8a17a0f28 100644
--- a/components/locales/en-US.js
+++ b/components/locales/en-US.js
@@ -5,54 +5,25 @@ export default {
datePicker: {
ok: 'OK',
to: 'to',
- placeholder: 'Select Date',
- placeholderTimeperiod: 'Select Date Time',
+ placeholder: ['Select Date'],
+ placeholderTimeperiod: ['Select Time'],
dateChoose: 'Select Date',
timeChoose: 'Select Time',
undefinedType: 'undefined type',
lastWeek: 'Nearly week',
lastMonth: 'Nearly month',
lastThreeMonth: 'Nearly three months',
- lastSixMonth: 'Nearly six months',
lastYear: 'Nearly year',
- month: [
- 'January',
- 'February',
- 'March',
- 'April',
- 'May',
- 'June',
- 'July',
- 'August',
- 'September',
- 'October',
- 'November',
- 'December'
- ],
- monthShort: [
- 'Jan',
- 'Feb',
- 'Mar',
- 'Apr',
- 'May',
- 'Jun',
- 'Jul',
- 'Aug',
- 'Sep',
- 'Oct',
- 'Nov',
- 'Dec'
- ],
+ month: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
+ monthShort: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
week: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
placeholders: {
- date: 'Select Date',
- month: 'Select Month',
- year: 'Select Year',
- time: 'Select Time',
+ date: ['Select Date'],
+ month: ['Select Month'],
+ year: ['Select Year'],
+ time: ['Select Time'],
daterange: ['Select Start Date', 'Select End Date'],
- yearrange: ['Select Start Year', 'Select End Year'],
- monthrange: ['Select Start Month', 'Select End Month'],
- week: 'Select Week',
+ week: ['Select Week'],
weekrange: ['Select Start Week', 'Select End Week'],
timeperiod: ['Select Start Date Time', 'Select Start End Time']
},
@@ -80,7 +51,17 @@ export default {
placeholder: 'Please select',
emptyContent: 'Not found',
searchPlaceholder: 'Please search',
- checkAll: 'Check all'
+ checkAll: 'Check all',
+ justSelected: 'Just Selected'
+ },
+ selectTree: {
+ back: 'Back',
+ search: 'search'
+ },
+ search: {
+ searchEmptyResult: 'No serach results',
+ searchEmptyRecord: 'No serach record',
+ searchRecord: 'Serach record'
},
transfer: {
checkAll: 'Check all',
@@ -130,5 +111,8 @@ export default {
searchEmptyResult: 'No serach results',
modalTitle: 'Warning',
delTips: 'Deleting a node will delete all child nodes, are you sure to delete this node?'
+ },
+ watermark: {
+ content: 'Please do not gaiden'
}
}
diff --git a/components/locales/zh-CN.js b/components/locales/zh-CN.js
index f3120c148..a3e7c2fad 100644
--- a/components/locales/zh-CN.js
+++ b/components/locales/zh-CN.js
@@ -5,30 +5,28 @@ export default {
datePicker: {
ok: '确认',
to: '至',
- placeholder: '请选择日期',
- placeholderTimeperiod: '请选择日期时间',
+ placeholder: ['请选择日期'],
+ placeholderTimeperiod: ['请选择时间'],
dateChoose: '日期选择',
timeChoose: '时间选择',
undefinedType: '类型未定义',
lastWeek: '近一周',
lastMonth: '近一月',
lastThreeMonth: '近三月',
- lastSixMonth: '近半年',
lastYear: '近一年',
month: ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月'],
monthShort: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'],
week: ['日', '一', '二', '三', '四', '五', '六'],
placeholders: {
- date: '请选择日期',
- month: '请选择月',
- year: '请选择年',
- time: '请选择时间',
+ date: ['请选择日期'],
+ month: ['请选择月'],
+ year: ['请选择年'],
+ time: ['请选择时间'],
daterange: ['开始日期', '结束日期'],
- yearrange: ['开始年', '结束年'],
- monthrange: ['开始月', '结束月'],
- week: '请选择周',
+ week: ['请选择周'],
weekrange: ['开始周', '结束周'],
- timeperiod: ['开始日期时间', '结束日期时间']
+ timeperiod: ['开始时间', '结束时间'],
+ timerange: ['请选择开始时间', '请选择结束时间']
},
year: '年',
timePeriod: '时间段',
@@ -49,13 +47,24 @@ export default {
},
cascader: {
placeholder: '请选择',
- noFoundTip: '无匹配数据'
+ noFoundTip: '无匹配数据',
+ emptyContent: '暂无数据'
},
select: {
placeholder: '请选择',
emptyContent: '无匹配数据',
searchPlaceholder: '搜索',
- checkAll: '全选'
+ checkAll: '全选',
+ justSelected: '仅看已选'
+ },
+ selectTree: {
+ back: '返回',
+ search: '搜索'
+ },
+ search: {
+ searchEmptyResult: '未找到搜索结果',
+ searchEmptyRecord: '无搜索记录',
+ searchRecord: '搜索历史'
},
transfer: {
checkAll: '全选',
@@ -105,5 +114,8 @@ export default {
searchEmptyResult: '未找到搜索结果',
modalTitle: '提示',
delTips: '删除节点将删除所有子节点,确定删除吗?'
+ },
+ watermark: {
+ content: '请勿外传'
}
}
diff --git a/components/menu/Item.js b/components/menu/Item.js
index 9bd3db840..795598819 100644
--- a/components/menu/Item.js
+++ b/components/menu/Item.js
@@ -4,7 +4,7 @@ import classNames from 'classnames'
import Title from './Title'
class Item extends Component {
- render () {
+ render() {
const { children, disabled, onClick, level, placement, activeIndex, id, icon, index, data, mini } = this.props
const isActive = activeIndex === index
const cls = classNames('hi-menu-item', 'hi-menu-item__title', 'hi-menu__title', `hi-menu--${level}`, {
diff --git a/components/menu/Menu.js b/components/menu/Menu.js
index 24e6ea824..517051381 100644
--- a/components/menu/Menu.js
+++ b/components/menu/Menu.js
@@ -7,7 +7,7 @@ import Item from './Item'
import SubMenu from './SubMenu'
import './style/index'
class Menu extends Component {
- constructor (props) {
+ constructor(props) {
super(props)
const { activeId, collapsed } = this.props
const activeIndex = this.getActiveIndex(activeId)
@@ -26,7 +26,7 @@ class Menu extends Component {
this.clickInsideFlag = false // click在menu标识
}
- componentWillReceiveProps (nextProps) {
+ componentWillReceiveProps(nextProps) {
const { activeId, data, collapsed } = nextProps
if (activeId !== this.props.activeId || !_.isEqual(data, this.props.data)) {
const activeIndex = this.getActiveIndex(activeId, data)
@@ -48,15 +48,15 @@ class Menu extends Component {
}
}
- componentDidMount () {
+ componentDidMount() {
window.addEventListener('click', this.clickOutsideHandel)
}
- componentWillUnmount () {
+ componentWillUnmount() {
window.removeEventListener('click', this.clickOutsideHandel)
}
- clickOutside () {
+ clickOutside() {
if (!this.clickInsideFlag && !this.isNoMiniVertaicalMenu()) {
this.setState({
expandIndex: []
@@ -66,11 +66,11 @@ class Menu extends Component {
this.clickInsideFlag = false
}
- clickInside () {
+ clickInside() {
this.clickInsideFlag = true
}
- getExpandIndex (clickedIndex) {
+ getExpandIndex(clickedIndex) {
if (!clickedIndex) {
return []
}
@@ -79,7 +79,7 @@ class Menu extends Component {
let _clickedIndex = clickedIndex
let subInExpandIndex = false
- let _expandIndex = expandIndex.filter((item) => {
+ const _expandIndex = expandIndex.filter((item) => {
// 点击父菜单时,需要把已展开的子菜单过滤掉,因为父菜单关闭时所有子菜单也要关闭
const flag = item.startsWith(_clickedIndex)
if (flag) {
@@ -106,15 +106,15 @@ class Menu extends Component {
}
}
- isNoMiniVertaicalMenu (collapsed = this.state.collapsed) {
+ isNoMiniVertaicalMenu(collapsed = this.state.collapsed) {
// 垂直非mini菜单
return this.props.placement === 'vertical' && !collapsed
}
- getActiveMenus (menus, activeId, activeMenus = []) {
+ getActiveMenus(menus, activeId, activeMenus = []) {
let result
- for (let index in menus) {
- let _activeMenus = [...activeMenus]
+ for (const index in menus) {
+ const _activeMenus = [...activeMenus]
if (menus[index].id === activeId) {
_activeMenus.push(index)
result = _activeMenus
@@ -131,7 +131,7 @@ class Menu extends Component {
}
}
- getActiveIndex (activeId, menu) {
+ getActiveIndex(activeId, menu) {
// 获取激活item对应的索引,以'-'拼接成字符串
const { data } = this.props
@@ -142,7 +142,7 @@ class Menu extends Component {
return (activeMenus && activeMenus.join('-')) || ''
}
- toggleMini () {
+ toggleMini() {
const collapsed = !this.state.collapsed
const expandIndex = collapsed ? [] : this.state.expandIndex
@@ -159,7 +159,7 @@ class Menu extends Component {
}, 0)
}
- onClick (indexs, id, data) {
+ onClick(indexs, id, data) {
const expandIndex = this.isNoMiniVertaicalMenu() ? this.state.expandIndex : this.getExpandIndex('') // 非mini垂直菜单选中时不需要收起子菜单
const oldId = this.state.activeId
@@ -175,7 +175,7 @@ class Menu extends Component {
)
}
- onClickSubMenu (index) {
+ onClickSubMenu(index) {
const expandIndex = this.getExpandIndex(index)
this.clickInside()
@@ -189,7 +189,7 @@ class Menu extends Component {
)
}
- renderItem (data, index, props = {}) {
+ renderItem(data, index, props = {}) {
// render menu item
const { activeIndex } = this.state
const mergeProps = Object.assign(
@@ -209,18 +209,18 @@ class Menu extends Component {
return - {data.content}
}
- renderFatSubMenu (data, parentIndex) {
+ renderFatSubMenu(data, parentIndex) {
// render胖菜单
- let groups = []
+ const groups = []
data.forEach((dataItem, groupIndex) => {
groups.push(
- -
-
+
-
+
{dataItem.children && (
-
+
{dataItem.children.map((child, index) => {
return this.renderItem(child, parentIndex + '-' + groupIndex + '-' + index, { level: 2 })
})}
@@ -232,10 +232,10 @@ class Menu extends Component {
return groups
}
- renderMenu (data, parentIndex = '') {
- const { showAllSubMenus, placement, theme } = this.props
+ renderMenu(data, parentIndex = '') {
+ const { showAllSubMenus, placement, theme, overlayClassName } = this.props
const { activeIndex, expandIndex, collapsed } = this.state
- let items = []
+ const items = []
const renderMenu = showAllSubMenus ? this.renderFatSubMenu.bind(this) : this.renderMenu.bind(this)
data.forEach((item, index) => {
const indexStr = parentIndex !== '' ? parentIndex + '-' + index : '' + index
@@ -246,6 +246,7 @@ class Menu extends Component {
-
+
{placement === 'vertical' && showCollapse && (
-
+
{miniIcon}
)}
diff --git a/components/menu/SubMenu.js b/components/menu/SubMenu.js
index e1a8d8837..45173e2ab 100644
--- a/components/menu/SubMenu.js
+++ b/components/menu/SubMenu.js
@@ -5,17 +5,17 @@ import Popper from '../popper'
import Icon from '../icon'
import Title from './Title'
class SubMenu extends Component {
- onClick (index) {
+ onClick(index) {
this.props.onClick(index)
}
- checkActive (activeIndex, index) {
+ checkActive(activeIndex, index) {
const indexArr = index.split('-')
const activeIndexArr = activeIndex.split('-')
return activeIndexArr.slice(0, indexArr.length).join('-') === index
}
- checkExpand (activeIndex, expandIndex, index) {
+ checkExpand(activeIndex, expandIndex, index) {
return expandIndex.some((item) => {
const indexArr = index.split('-')
const expandIndexArr = item.split('-')
@@ -23,8 +23,8 @@ class SubMenu extends Component {
})
}
- renderPopperMenu (deepSubmenu, isExpand) {
- const { mini, datas, index, renderMenu, fatMenu, clickInside, theme } = this.props
+ renderPopperMenu(deepSubmenu, isExpand) {
+ const { mini, datas, index, renderMenu, fatMenu, clickInside, theme, overlayClassName } = this.props
let leftGap
let topGap
let placement
@@ -46,6 +46,7 @@ class SubMenu extends Component {
zIndex={1050}
topGap={topGap}
leftGap={leftGap}
+ overlayClassName={overlayClassName}
className={classNames('hi-submenu__popper', `theme__${theme}`, {
'hi-submenu__popper--fat': fatMenu
})}
@@ -62,7 +63,7 @@ class SubMenu extends Component {
)
}
- renderVerticalMenu (isActive, isExpand) {
+ renderVerticalMenu(isActive, isExpand) {
const { datas, index, renderMenu, clickInside, theme } = this.props
return (
-
diff --git a/components/menu/index.d.ts b/components/menu/index.d.ts
new file mode 100644
index 000000000..d1da36b67
--- /dev/null
+++ b/components/menu/index.d.ts
@@ -0,0 +1,22 @@
+type DataItem = {
+ content: string | JSX.Element
+ icon?: string | JSX.Element
+ id: string | number
+ disabled?: boolean
+ children?: DataItem[]
+}
+interface Props {
+ data: DataItem[]
+ activeId?: string | number
+ placement?: 'horizontal' | 'vertical'
+ collapsed?: boolean
+ showCollapse?: boolean
+ showAllSubMenus?: boolean
+ accordion?: boolean
+ onClick?: (activeId: string | number, prevActiveId: string | number) => void
+ onClickSubMenu?: (subMenuIndexs: number) => void
+ onCollapse?: (collapsed: boolean) => void
+ overlayClassName?: string
+}
+declare const Menu: React.ComponentType
+export default Menu
diff --git a/components/menu/style/index.scss b/components/menu/style/index.scss
index 7e05d0a20..e890535d3 100644
--- a/components/menu/style/index.scss
+++ b/components/menu/style/index.scss
@@ -1,4 +1,4 @@
-@import '@hi-ui/core-css/index.scss';
+@import '../../core-css/index.scss';
@mixin ellipsis($max-width: initial, $width: auto) {
width: $width;
@@ -26,8 +26,6 @@
.hi-menu,
.hi-submenu__popper {
- @include component-reset();
-
ul {
margin-top: 0;
margin-bottom: 0;
@@ -48,17 +46,17 @@
&--active {
> .hi-menu__title .hi-menu__title-content,
> .hi-menu__title-content {
- color: #4284f5;
+ color: use-color('primary');
}
> .hi-menu__title .hi-menu__title-icon,
> .hi-menu__title-icon {
- color: #4284f5;
+ color: use-color('primary');
}
> .hi-submenu__title {
& > .hi-menu__title-toggle-icon {
- color: #4284f5;
+ color: use-color('primary');
}
}
}
@@ -66,7 +64,7 @@
&--disabled {
.hi-menu__title {
cursor: not-allowed;
- color: #d8d8d8;
+ color: use-color('gray-30');
}
}
}
@@ -75,7 +73,7 @@
display: flex;
line-height: 22px;
font-size: 14px;
- color: #333;
+ color: use-color('black');
cursor: pointer;
align-items: center;
@@ -103,7 +101,7 @@
.hi-menu {
&--horizontal {
- border-bottom: 1px solid #e6e7e8;
+ border-bottom: 1px solid use-color('gray-20');
.hi-menu-item {
position: relative;
@@ -117,7 +115,7 @@
transform: translateX(-50%);
width: 32px;
height: 2px;
- background-color: #4284f5;
+ background-color: use-color('primary');
}
&:last-child {
@@ -138,7 +136,7 @@
display: inline-block;
width: 216px;
padding: 12px 0;
- background: #fff;
+ background: use-color('white');
box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.15);
box-sizing: border-box;
@@ -151,14 +149,14 @@
position: relative;
&--active:not(.hi-submenu) {
- background-color: rgba(66, 133, 244, 0.08);
+ background-color: use-color('primary-20');
&::after {
content: '';
position: absolute;
width: 2px;
height: 100%;
- background-color: #4284f5;
+ background-color: use-color('primary');
left: 0;
}
}
@@ -169,7 +167,7 @@
height: 48px;
&:hover {
- background-color: rgba(66, 133, 244, 0.08);
+ background-color: use-color('primary-20');
}
}
@@ -188,7 +186,7 @@
overflow: hidden;
.hi-menu--1.hi-menu-item--active {
- background-color: rgba(66, 133, 244, 0.08);
+ background-color: use-color('primary-20');
}
.hi-menu__title {
@@ -199,14 +197,14 @@
position: relative;
&--active {
- background-color: rgba(66, 133, 244, 0.08);
+ background-color: use-color('primary-20');
&::after {
content: '';
position: absolute;
width: 2px;
height: 100%;
- background-color: #4284f5;
+ background-color: use-color('primary');
left: 0;
}
}
@@ -239,9 +237,9 @@
&__popper {
width: 200px;
padding: 8px 0;
- background-color: #fff;
+ background-color: use-color('white');
box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.15);
- border: 1px solid rgba(230, 231, 232, 1);
+ border: 1px solid use-color('gray-20');
box-sizing: border-box;
.hi-submenu__items {
@@ -250,7 +248,7 @@
.hi-menu-item--disabled {
cursor: not-allowed;
- color: #d8d8d8;
+ color: use-color('gray-30');
}
}
@@ -271,12 +269,12 @@
width: 120px;
&__title {
- color: #999;
+ color: use-color('gray-70');
}
&__content {
font-size: 14px;
- color: #999;
+ color: use-color('gray-70');
box-sizing: border-box;
@include ellipsis();
@@ -284,138 +282,3 @@
}
}
}
-
-@each $key, $value in $palette-primary {
- .theme__#{$key}.hi-menu {
- .hi-menu-item {
- &--active {
- > .hi-menu__title .hi-menu__title-content,
- > .hi-menu__title-content {
- color: $value;
- }
-
- > .hi-menu__title .hi-menu__title-icon,
- > .hi-menu__title-icon {
- color: $value;
- }
-
- > .hi-submenu__title {
- & > .hi-menu__title-toggle-icon {
- color: $value;
- }
- }
- }
-
- &--disabled {
- .hi-menu__title {
- cursor: not-allowed;
- color: #d8d8d8;
- }
- }
-
- &--active::after {
- background-color: $value;
- }
- }
-
- &.active {
- border-color: $value;
- }
-
- &--mini {
- .hi-menu-item {
- &--active {
- background-color: rgba($value, 0.1);
-
- &::after {
- background-color: $value;
- }
- }
- }
- }
-
- &--vertical {
- box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.15);
-
- .hi-menu-item {
- &--active:not(.hi-submenu) {
- background-color: rgba($value, 0.1);
-
- &::after {
- background-color: $value;
- }
- }
- }
-
- .hi-menu__title {
- &:hover {
- background-color: rgba($value, 0.1);
- }
- }
- }
- }
-
- .theme__#{$key}.hi-menu-item {
- &--active {
- > .hi-menu__title .hi-menu__title-content,
- > .hi-menu__title-content {
- color: $value;
- }
-
- > .hi-menu__title .hi-menu__title-icon,
- > .hi-menu__title-icon {
- color: $value;
- }
-
- > .hi-submenu__title {
- & > .hi-menu__title-toggle-icon {
- color: $value;
- }
- }
- }
-
- &--disabled {
- .hi-menu__title {
- cursor: not-allowed;
- color: #d8d8d8;
- }
- }
-
- &--active::after {
- background-color: $value;
- }
- }
-
- .theme__#{$key}.hi-submenu__popper {
- .hi-menu-item {
- &--active {
- > .hi-menu__title .hi-menu__title-content,
- > .hi-menu__title-content {
- color: $value;
- }
-
- > .hi-menu__title .hi-menu__title-icon,
- > .hi-menu__title-icon {
- color: $value;
- }
-
- > .hi-submenu__title {
- & > .hi-menu__title-toggle-icon {
- color: $value;
- }
- }
- }
-
- &--disabled {
- .hi-menu__title {
- cursor: not-allowed;
- color: #d8d8d8;
- }
- }
-
- &--active::after {
- background-color: $value;
- }
- }
- }
-}
diff --git a/components/message/index.d.ts b/components/message/index.d.ts
new file mode 100644
index 000000000..6893812e8
--- /dev/null
+++ b/components/message/index.d.ts
@@ -0,0 +1,13 @@
+interface Props {
+
+}
+type Options = {
+ type?: 'info' | 'success' | 'error' | 'warning'
+ title: string
+ duration?: number
+}
+const OpenFun: (options: Options) => void
+declare class Message extends React.Component {
+ static open = OpenFun
+}
+export default Message
diff --git a/components/message/index.js b/components/message/index.js
index 4be471ba1..6c9ee18be 100755
--- a/components/message/index.js
+++ b/components/message/index.js
@@ -1,30 +1,23 @@
import notice from '../notice'
import './style/index'
import React from 'react'
-import classNames from 'classnames'
+import Icon from '../icon'
const iconMap = {
- success: 'chenggong',
- error: 'shibai',
- warning: 'jinggao',
- info: 'tishi'
+ success: 'check-circle',
+ danger: 'close-circle',
+ warning: 'exclamation-circle',
+ primary: 'info-circle'
}
const message = {
- open: ({
- title,
- prefix = 'message',
- key = Math.random(),
- duration,
- closeable = false,
- type = 'info'
- }) => {
+ open: ({ title, prefix = 'message', key = Math.random(), duration, closeable = false, type = 'info' }) => {
+ let _type = type === 'info' ? 'primary' : type
+ _type = type === 'error' ? 'danger' : _type
const NoticeContent = (
-
-
-
-
+
@@ -35,7 +28,7 @@ const message = {
key,
closeable,
duration,
- type
+ type: _type
})
}
}
diff --git a/components/message/style/index.scss b/components/message/style/index.scss
index 5dd578915..3aa840abd 100755
--- a/components/message/style/index.scss
+++ b/components/message/style/index.scss
@@ -1,13 +1,10 @@
-@import '@hi-ui/core-css/index.scss';
-@import '../../style/icon/index.scss';
+@import '../../core-css/index.scss';
-$theme-colors-notice: (
- 'orange': #ffefea,
- 'cyan': #ecf8f4 ,
- 'magenta': #ffeef1,
- 'lavender': #f7edfb,
- 'blue': #ebf6fe,
- 'purple': #8a8acb
+$color-map: (
+ 'primary': use-color('primary'),
+ 'warning': get-color($palette-secondary, 'warning'),
+ 'success': get-color($palette-secondary, 'success'),
+ 'danger': get-color($palette-secondary, 'danger')
) !default;
.hi-message__container {
@@ -19,7 +16,7 @@ $theme-colors-notice: (
.hi-message {
position: relative;
- padding: 7px 12px;
+ padding: 9px 12px;
margin-bottom: 10px;
border-radius: 2px;
transition: 0.3s ease;
@@ -27,77 +24,35 @@ $theme-colors-notice: (
align-items: flex-start;
width: 260px;
box-sizing: border-box;
- }
-
- .hi-message__title--wrapper {
- display: flex;
- align-items: center;
-
- .hi-message__title {
- color: #333 !important;
- font-size: 14px;
- }
- }
-
- .hi-message__icon {
- font-size: 16px;
- margin-right: 12px;
- }
- .hi-message--info {
- background: #edf2fc;
- border: 1px solid #d8e5ff;
+ @each $key, $value in $color-map {
+ &--#{$key} {
+ background-color: use-color(#{$key}-20);
+ border: 1px solid use-color(#{$key}-40);
- .hi-message__title,
- .hi-message__icon {
- color: #4284f5;
- }
-
- .hi-message__button {
- background-color: #4284f5;
+ .hi-message__icon {
+ fill: $value;
+ }
+ }
}
}
- .hi-message--error {
- background: #feeff0;
- border: 1px solid #ffe0e3;
-
- .hi-message__title,
- .hi-message__icon {
- color: #eb5252;
- }
-
- .hi-message__button {
- background-color: #eb5252;
- }
+ .hi-message__header {
+ line-height: $line-height-size-normal;
+ display: flex;
+ align-items: center;
}
- .hi-message--success {
- background: #e8f6ee;
- border: 1px solid #cae9d7;
-
- .hi-message__title,
- .hi-message__icon {
- color: #1da653;
- }
-
- .hi-message__button {
- background-color: #1da653;
- }
+ .hi-message__title {
+ color: use-color('black') !important;
+ font-size: $font-size-normal;
}
- .hi-message--warning {
- background: #fbf5e6;
- border: 1px solid #fcf0d4;
-
- .hi-message__title,
- .hi-message__icon {
- color: #e19d0c;
- }
-
- .hi-message__button {
- background-color: #e19d0c;
- }
+ .hi-message__icon {
+ width: 16px;
+ height: 16px;
+ margin-right: 8px;
+ flex-shrink: 0;
}
.hi-message-enter {
@@ -116,31 +71,3 @@ $theme-colors-notice: (
transform: translateY(-100%);
}
}
-
-@each $key, $value in $theme-colors {
- .hi-message__container {
- .theme__#{$key}.hi-message.hi-message--info {
- border:
- 1px
- solid
- map-get(get-palette(get-color($palette-primary, $key)), '20');
-
- .hi-icon {
- color: $value;
- }
-
- .hi-message__button {
- background-color: $value;
- border: 1px solid $value;
- }
- }
- }
-}
-
-@each $key, $value in $theme-colors-notice {
- .hi-message__container {
- .theme__#{$key}.hi-message.hi-message--info {
- background-color: $value;
- }
- }
-}
diff --git a/components/modal/index.d.ts b/components/modal/index.d.ts
new file mode 100644
index 000000000..346619b59
--- /dev/null
+++ b/components/modal/index.d.ts
@@ -0,0 +1,16 @@
+import { CSSProperties } from "react"
+interface Props {
+ title?: string | JSX.Element
+ visible?: boolean
+ closeable?: boolean
+ maskCloseable?: boolean
+ cancelText?: string
+ confirmText?: string
+ size?: 'default' | 'large'
+ style?: CSSProperties
+ footer?: JSX.Element | null
+ onCancel?: (e: MouseEvent) => void
+ onConfirm?: (e: MouseEvent) => void
+}
+declare const Modal: React.ComponentType
+export default Modal
diff --git a/components/modal/index.js b/components/modal/index.js
deleted file mode 100755
index 694fbbcdc..000000000
--- a/components/modal/index.js
+++ /dev/null
@@ -1,158 +0,0 @@
-import React, { Component } from 'react'
-import { createPortal } from 'react-dom'
-import PropTypes from 'prop-types'
-import classNames from 'classnames'
-import Button from '../button'
-import Icon from '../icon'
-import './style/index'
-import Provider from '../context'
-import confirm from '../confirm'
-
-class Modal extends Component {
- static propTypes = {
- title: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
- size: PropTypes.oneOf(['large', 'default', 'small']),
- show: PropTypes.bool, // TODO: 废弃,使用 visible
- visible: PropTypes.bool,
- onConfirm: PropTypes.func,
- onCancel: PropTypes.func,
- confirmText: PropTypes.string,
- cancelText: PropTypes.string,
- backDrop: PropTypes.bool, // TODO: 废弃,使用 maskCloseable
- maskCloseable: PropTypes.bool,
- confirmType: PropTypes.string,
- cancelType: PropTypes.string,
- closeBtn: PropTypes.bool, // TODO: 废弃,使用 maskCloseable
- closabble: PropTypes.bool
- }
-
- static defaultProps = {
- title: '',
- adaptive: false,
- backDrop: true,
- maskCloseable: true,
- show: false,
- visible: false,
- closeBtn: true,
- closeable: true,
- destory: false,
- size: 'default'
- }
- constructor (props) {
- super(props)
- this.state = {}
- this.node = document.createElement('div')
- document.body.appendChild(this.node)
- }
- static getDerivedStateFromProps (props, state) {
- // 屏蔽滚动条
- if (props.show || props.visible) {
- document.body.style.setProperty('overflow', 'hidden')
- } else {
- document.body.style.removeProperty('overflow')
- }
- return null
- }
- componentWillUnmount () {
- this._unblock()
- }
- _unblock () {
- this.props.destory && document.body.removeChild(this.node)
- document.body.style.removeProperty('overflow')
- }
- handleConfirm () {
- if (this.props.onConfirm) {
- this.props.onConfirm()
- }
- this._unblock()
- }
- handleClose () {
- if (this.props.onCancel) {
- this.props.onCancel()
- }
- this._unblock()
- }
- localeDatasProps (key) {
- const { localeDatas } = this.props
- if (this.props[key]) {
- return this.props[key]
- } else {
- return localeDatas.modal[key]
- }
- }
- renderFooter () {
- const { footers, footer } = this.props
- const cancelText = this.localeDatasProps('cancelText')
- const confirmText = this.localeDatasProps('confirmText')
- if (footers || footer) {
- return footers || footer
- } else {
- return [
- ,
-
- ]
- }
- }
- render () {
- const {
- width,
- style = {},
- title,
- children,
- backDrop,
- maskCloseable,
- closeBtn,
- closeable,
- footers, // TODO: 废弃,使用 footer 代替
- footer,
- show,
- visible,
- size,
- className
- } = this.props
- let _style = width ? Object.assign({}, { width }, { ...style }) : style
- let classnames = classNames('hi-modal', show || visible ? '' : 'hi-modal--hide', className)
- return createPortal(
-
-
{
- ((backDrop && maskCloseable !== false) || (maskCloseable && backDrop !== false)) &&
- this.handleClose.apply(this)
- }}
- />
-
- {(title || (closeBtn && closeable !== false) || (closeable && closeBtn !== false)) && (
-
-
{title}
- {((closeBtn && closeable !== false) || (closeable && closeBtn !== false)) && (
-
-
-
- )}
-
- )}
-
{children}
- {(((!footers || footers.length > 0) && footer !== null) || footer) && (
-
{this.renderFooter()}
- )}
-
-
,
- this.node
- )
- }
-}
-
-const ModalWrapper = Provider(Modal)
-ModalWrapper.confirm = confirm
-export default ModalWrapper
diff --git a/components/modal/index.jsx b/components/modal/index.jsx
new file mode 100644
index 000000000..9af9aabe0
--- /dev/null
+++ b/components/modal/index.jsx
@@ -0,0 +1,209 @@
+import React, { useEffect, useRef, useState, useCallback } from 'react'
+import { render, unmountComponentAtNode, createPortal } from 'react-dom'
+import { CSSTransition } from 'react-transition-group'
+import Classnames from 'classnames'
+import Provider from '../context/index'
+import Button from '../button'
+import Icon from '../icon'
+import './style/index'
+
+const PREFIX = 'hi-modal'
+
+const getDefaultContainer = () => {
+ const defaultContainer = document.createElement('div')
+ document.body.appendChild(defaultContainer)
+ return defaultContainer
+}
+
+const InternalModalComp = ({
+ children,
+ container,
+ visible,
+ title,
+ onConfirm,
+ onCancel,
+ maskClosable = true,
+ width,
+ height,
+ size = 'default',
+ showHeaderDivider = true,
+ showFooterDivider = true,
+ footer,
+ confirmText,
+ cancelText,
+ style,
+ className,
+ destroyOnClose,
+ localeDatas
+}) => {
+ // TODO: 整体可以抽成一个 hooks 供 modal 和 drawer 复用
+ const defaultContainer = useRef(false)
+ if (defaultContainer.current === false && !container) {
+ defaultContainer.current = getDefaultContainer()
+ }
+
+ const [vi, setVi] = useState(false)
+ useEffect(() => {
+ visible && setVi(true)
+ }, [visible])
+
+ const destroy = useCallback(() => {
+ const _container = container || defaultContainer.current
+ unmountComponentAtNode(_container)
+ _container.parentNode.removeChild(_container)
+ }, [])
+
+ useEffect(() => {
+ const parent = (container || defaultContainer.current).parentNode
+ // 屏蔽滚动条
+ if (vi) {
+ parent.style.setProperty('overflow', 'hidden')
+ } else {
+ parent.style.removeProperty('overflow')
+ if (destroyOnClose) {
+ destroy()
+ }
+ }
+ }, [vi])
+
+ return createPortal(
+
+
{
+ if (maskClosable && onCancel) {
+ onCancel()
+ }
+ }}
+ />
+
+
{
+ setTimeout(() => setVi(false), 300)
+ }}
+ >
+
+
+ {title}
+ {
+ if (onCancel) {
+ onCancel()
+ }
+ }}
+ />
+
+
{children}
+ {footer !== null && (
+
+ {footer === undefined && cancelText !== null && (
+
+ )}
+ {footer === undefined && confirmText !== null && (
+
+ )}
+ {footer}
+
+ )}
+
+
+
+
,
+ container || defaultContainer.current
+ )
+}
+
+const confirmIconMap = {
+ success: { name: 'check-circle', color: '#1DA653' },
+ error: { name: 'close-circle', color: '#EB5252' },
+ warning: { name: 'exclamation-circle', color: '#e19d0b' },
+ info: { name: 'info-circle', color: '#4284F5' }
+}
+
+const confirm = ({ onConfirm, onCancel, title = '提示', content, type = 'default', confirmText, cancelText }) => {
+ const confirmContainer = document.createElement('div')
+
+ document.body.appendChild(confirmContainer)
+ const modal = React.createElement(ModalComp, {
+ container: confirmContainer,
+ title,
+ width: 480,
+ height: 240,
+ visible: true,
+ confirmText,
+ cancelText,
+ onConfirm: () => {
+ onConfirm && onConfirm()
+ confirmContainer.parentNode.style.removeProperty('overflow')
+ unmountComponentAtNode(confirmContainer)
+ confirmContainer.parentNode.removeChild(confirmContainer)
+ },
+ showFooterDivider: false,
+ children: (
+
+ {type !== 'default' && (
+
+ )}
+ {content}
+
+ ),
+ onCancel: () => {
+ onCancel && onCancel()
+ confirmContainer.parentNode.style.removeProperty('overflow')
+ unmountComponentAtNode(confirmContainer)
+ confirmContainer.parentNode.removeChild(confirmContainer)
+ }
+ })
+ render(modal, confirmContainer)
+}
+
+const ModalComp = Provider(InternalModalComp)
+ModalComp.confirm = confirm
+export default ModalComp
diff --git a/components/modal/style/index.js b/components/modal/style/index.js
index 14dd01250..63810a681 100644
--- a/components/modal/style/index.js
+++ b/components/modal/style/index.js
@@ -1,2 +1 @@
import './index.scss'
-import '../../style/icon/index.scss'
diff --git a/components/modal/style/index.scss b/components/modal/style/index.scss
old mode 100755
new mode 100644
index a6c112489..be21726e4
--- a/components/modal/style/index.scss
+++ b/components/modal/style/index.scss
@@ -1,103 +1,122 @@
-@import '@hi-ui/core-css/index.scss';
-
-.hi-modal {
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- z-index: 1000;
-
- &--hide {
- display: none !important;
- }
+@import '../../core-css/index.scss';
+$modal: 'hi-modal' !default;
+
+.#{$modal} {
+ color: use-color('black');
+ font-size: $font-size-normal;
&__mask {
- position: absolute;
+ position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
- z-index: 1000; // @Todo: should be larger
- width: 100%;
- height: 100%;
- background-color: rgba(0, 0, 0, 0.54);
- transition: all $speed-fast $easing;
+ height: 0;
+ z-index: 1000;
+ background: rgba(0, 0, 0, 0.45);
+ opacity: 0;
+ transition: opacity 0.3s, height 0s 0.3s;
+
+ &--visible {
+ opacity: 1;
+ height: 100%;
+ transition: opacity 0.3s;
+ }
}
- &__dialog {
- position: relative;
- top: 10%;
- z-index: 1001;
- box-sizing: border-box;
- width: 600px;
- max-height: 80%;
- margin: 0 auto;
- border-radius: 2px;
+ &__container {
+ z-index: 1000;
+ top: 100px;
+ left: 50%;
+ transform: translateX(-50%);
+ position: fixed;
+ }
+
+ &__wrapper {
display: flex;
flex-direction: column;
- font-size: $font-size-normal;
- background-color: $white;
- box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.1);
- transition: all $speed-fast $easing;
+ background: use-color('white');
+ border-radius: 2px;
+ box-shadow: 0 5px 15px 0 rgba(0, 0, 0, 0.1);
+
+ &--default {
+ width: 600px;
+ max-height: 600px;
+ min-height: 240px;
+ }
&--large {
width: 800px;
+ max-height: 600px;
+ min-height: 240px;
}
&--small {
width: 360px;
+ max-height: 600px;
+ min-height: 240px;
}
}
&__header {
- flex: none;
+ font-size: $font-size-large;
+ color: use-color('black');
+ font-weight: $font-weight-bold;
+ height: 54px;
+ box-sizing: border-box;
+ padding: 0 24px;
display: flex;
align-items: center;
- height: 56px;
- border-bottom: 1px solid $gray-light;
- }
+ justify-content: space-between;
+ flex-shrink: 0;
- &__title {
- flex: 1;
- margin: 0;
- padding: 0 $spacer-4;
- font-size: $font-size-large;
- font-weight: $font-weight-semibold;
- text-align: left;
- color: $black;
- }
-
- &__close {
- width: 56px;
- height: 56px;
- font-size: $font-size-large;
- line-height: 56px;
- text-align: center;
- color: $gray-darker;
- cursor: pointer;
- transition: color $speed-fast $easing;
-
- &:hover,
- &:active {
- color: $black;
+ &--divided {
+ border-bottom: 1px solid use-color('gray-20');
}
}
&__content {
- flex: auto;
+ flex: 1;
box-sizing: border-box;
- min-height: 54px;
- padding: $spacer-4;
- overflow-x: hidden;
- overflow-y: auto;
+ overflow: auto;
+ padding: 24px;
}
&__footer {
- flex: none;
+ height: 54px;
+ box-sizing: border-box;
+ padding: 0 24px;
display: flex;
- border-top: 1px solid $gray-light;
- padding: $spacer-3 $spacer-4;
+ align-items: center;
justify-content: flex-end;
+ flex-shrink: 0;
+
+ &--divided {
+ border-top: 1px solid use-color('gray-20');
+ }
+ }
+
+ .modal-transition {
+ &-enter {
+ opacity: 0;
+ transform: scale(0.4);
+ }
+
+ &-enter-done {
+ opacity: 1;
+ transform: scale(1);
+ transition: opacity 0.3s, transform 0.3s;
+ }
+
+ &-exit {
+ opacity: 1;
+ transform: scale(1);
+ }
+
+ &-exit-done {
+ opacity: 0;
+ transform: scale(0.4);
+ transition: opacity 0.3s, transform 0.3s;
+ }
}
}
diff --git a/components/nav-menu/NavMenu.js b/components/nav-menu/NavMenu.js
deleted file mode 100644
index 818932500..000000000
--- a/components/nav-menu/NavMenu.js
+++ /dev/null
@@ -1,211 +0,0 @@
-/**
- * @Author lishuaishuai
- * @Date 2018-04-02 16:56:13
- * @Description NavMenu 导航菜单
- */
-import React, { Component, cloneElement } from 'react'
-import PropTypes from 'prop-types'
-import classNames from 'classnames'
-
-class NavMenu extends Component {
- static propTypes = {
- data: PropTypes.arrayOf(PropTypes.object).isRequired,
- selectedKey: PropTypes.number,
- onClick: PropTypes.func,
- render: PropTypes.func
- }
- static defaultProps = {
- prefixCls: 'hi-navmenu',
- selectedKey: 0,
- selectedSubKey: -1,
- vertical: false,
- width: '100%'
- }
- constructor (props) {
- super(props)
-
- this.getH = null
-
- this.state = {
- selectedKey: props.selectedKey,
- selectedSubKey: props.selectedSubKey,
- toggleShow: false,
- toggleOn: false,
- subMenuShow: false,
- subMenuChildred: []
- }
- this.setToggle = this.setToggle().bind(this)
- }
-
- componentWillReceiveProps (props) {
- if (this.props.selectedKey !== props.selectedKey) {
- this.setState({
- selectedKey: props.selectedKey
- })
- }
- }
-
- componentDidMount () {
- const {
- selectedKey,
- data
- } = this.props
- if (selectedKey < data.length) {
- this.handleClick(data[selectedKey], selectedKey, 1)
- }
- this.getH && this.setToggleEvent(this.getH)
- window.addEventListener('resize', this.setToggle)
- }
-
- componentWillUnmount () {
- window.removeEventListener('resize', this.setToggle)
- }
-
- setToggleEvent (el) {
- const ulH = el.scrollHeight
- const {vertical} = this.props
- let toggleShow = false
- if (ulH > 50 && !vertical) {
- toggleShow = true
- }
-
- this.setState({ toggleShow })
- }
-
- setToggle () {
- let start = Date.now()
- const setToggleEvent = this.setToggleEvent.bind(this)
-
- return () => {
- const now = Date.now()
- if (now - start > 200 && this.getH) {
- setToggleEvent(this.getH)
- start = now
- }
- }
- }
-
- handleToggle = () => {
- this.setState({
- toggleOn: !this.state.toggleOn
- })
- }
- handleClick = (item, index, level, e) => {
- const { onClick } = this.props
- const { selectedKey, subMenuShow } = this.state
- const { disabled, children = [] } = item
-
- if (disabled) return
-
- if (level !== 2) {
- if (selectedKey === index) {
- this.setState({
- subMenuShow: !subMenuShow
- })
- } else {
- this.setState({
- subMenuShow: true
- })
- }
- this.setState({
- subMenuChildred: children,
- selectedKey: index,
- selectedSubKey: -1
- })
- onClick && e && onClick(e, String(index))
- } else {
- this.setState({
- selectedSubKey: index
- })
- onClick && e && onClick(e, `${selectedKey}-${index}`)
- }
- }
- renderMenuItem = () => {
- const { data, prefixCls, render } = this.props
- const { selectedKey } = this.state
-
- return data.map((item, index) => {
- const classes = classNames(`${prefixCls}__item`, {
- 'on': selectedKey === index,
- 'hi-navmenu__item--disabled': item.disabled
- })
-
- return (
- -
- {render ? render(item) : item.title}
-
- )
- })
- }
- renderSubMenuItem = () => {
- const { prefixCls, render } = this.props
- const { selectedSubKey, subMenuChildred } = this.state
-
- return subMenuChildred.map((item, index) => {
- const classes = classNames(`${prefixCls}__sub`, {
- 'on': selectedSubKey === index,
- 'hi-navmenu__item--disabled': item.disabled
- })
-
- return (
- -
- {render ? render(item.title) : item.title}
-
- )
- })
- }
- rednerTabContent = () => {
- const { prefixCls, children } = this.props
- const { selectedKey } = this.state
- return React.Children.map(children, (item, index) => {
- const tabCtxCls = classNames(`${prefixCls}__content-item`, item.props.className, {
- 'hi-navmenu__content-item--show': selectedKey === index
- })
- return cloneElement(item, {
- className: tabCtxCls
- })
- })
- }
- render () {
- const {prefixCls, children, vertical, width} = this.props
- const {toggleOn, subMenuShow, toggleShow, subMenuChildred} = this.state
- const togClasses = classNames(`${prefixCls}__toggle`, {
- 'hi-navmenu__toggle--show': toggleShow,
- 'hi-navmenu__toggle--on': toggleOn
- })
- const ulClasses = classNames(`${prefixCls}__list`, {
- 'hi-navmenu__list--vertical': vertical,
- 'hi-navmenu__list--on': toggleOn
- })
- const subMenCls = classNames(`${prefixCls}__sublist`, {
- 'hi-navmenu__sublist--on': subMenuShow
- })
- const _verticalStyle = vertical ? {width: typeof width === 'number' ? `${width}px` : width} : {}
- return (
-
-
-
{ this.getH = arg }}
- className={ulClasses}
- style={_verticalStyle}
- >
- {this.renderMenuItem()}
-
- {subMenuChildred.length > 0 &&
{this.renderSubMenuItem()}
}
- {children &&
{this.rednerTabContent()}
}
-
- )
- }
-}
-export default NavMenu
diff --git a/components/nav-menu/__test__/index.test.js b/components/nav-menu/__test__/index.test.js
deleted file mode 100644
index b2aeefec3..000000000
--- a/components/nav-menu/__test__/index.test.js
+++ /dev/null
@@ -1,129 +0,0 @@
-import React from 'react'
-import { mount } from 'enzyme'
-import NavMenu from '../'
-
-let wrapper
-const clickCallback = jest.fn(items => items)
-const options = [
- {title: 'Option 0'},
- {title: 'Option 1', disabled: true},
- {title: 'Option 2'},
- {title: 'Option 3'},
- {title: 'Option 4'}
-]
-
-describe('NavMenu', () => {
- afterEach(() => {
- wrapper && wrapper.unmount()
- clickCallback.mockClear()
- })
-
- it('基础用法', () => {
- wrapper = mount(
-
- 0
- 1
- 2
- 3
- 4
-
- )
- // 默认激活项
- expect(wrapper.find('.hi-navmenu__item').at(2).hasClass('on')).toBeTruthy()
- expect(wrapper.find('.hi-navmenu__content-item--show').text()).toEqual('2')
-
- // 禁止项不可点
- wrapper.find('.hi-navmenu__content-item').at(1).simulate('click')
- expect(clickCallback).toHaveBeenCalledTimes(0)
-
- // 选中第一项
- wrapper.find('.hi-navmenu__item').at(0).simulate('click')
- expect(clickCallback).toHaveBeenCalledTimes(1)
- expect(wrapper.find('.hi-navmenu__item').at(0).hasClass('on')).toBeTruthy()
- expect(wrapper.find('.hi-navmenu__content-item--show').text()).toEqual('0')
- })
-
- it('自定义渲染', () => {
- const datas = [
- {title: 'Option 0'},
- {title: '点我跳转', url: 'https://www.mi.com/'},
- {title: 'Option 2', icon: 'https://www.mi.com/favicon.ico'},
- {title: 'Option 3'},
- {title: 'Option 4'}
- ]
- wrapper = mount(
- {
- return data.icon ? {data.title} : data.url ? {data.title} : data.title
- }}
- >
- )
-
- expect(wrapper.find('.hi-navmenu__item').find('img').props().src).toEqual('https://www.mi.com/favicon.ico')
- expect(wrapper.find('.hi-navmenu__item').find('a').props().href).toEqual('https://www.mi.com/')
-
- // 选中第3项
- wrapper.find('.hi-navmenu__item').at(2).simulate('click')
- expect(clickCallback).toHaveBeenCalledTimes(1)
- expect(wrapper.find('.hi-navmenu__item').at(2).hasClass('on')).toBeTruthy()
- })
-
- it('二级菜单', () => {
- const datas = [
- {
- title: 'Option 0',
- children: [
- {title: 'Option 6'},
- {title: 'Option 6'}
- ]
- },
- {
- title: 'Option 1',
- disabled: true,
- children: [
- {title: 'Option 6'},
- {title: 'Option 6'}
- ]
- },
- {
- title: 'Option 2',
- children: [
- {title: 'Option 7'},
- {title: 'Option 7'}
- ]
- },
- {
- title: 'Option 3',
- children: [
- {title: 'Option 8'},
- {title: 'Option 8'}
- ]
- },
- {
- title: 'Option 4',
- children: [
- {title: 'Option 9'},
- {title: 'Option 9'}
- ]
- }
- ]
- wrapper = mount(
-
- )
-
- expect(wrapper.find('.hi-navmenu__sublist').find('.hi-navmenu__sub')).toHaveLength(2)
- expect(wrapper.find('.hi-navmenu__sublist').find('.hi-navmenu__sub .on')).toHaveLength(0)
- wrapper.find('.hi-navmenu__sublist').find('.hi-navmenu__sub').at(1).simulate('click')
- expect(wrapper.find('.hi-navmenu__sublist').find('.hi-navmenu__sub').at(1).hasClass('on')).toBeTruthy()
- })
-})
diff --git a/components/nav-menu/__tests__/index.test.js b/components/nav-menu/__tests__/index.test.js
deleted file mode 100644
index fd89f3f4b..000000000
--- a/components/nav-menu/__tests__/index.test.js
+++ /dev/null
@@ -1,16 +0,0 @@
-import React, { Fragment } from 'react'
-import { mount } from 'enzyme'
-import sinon, { fake, spy } from 'sinon'
-import simulant from 'simulant'
-import Message from '../index'
-
-/* eslint-env jest */
-
-describe('Message', () => {
- describe('Methods', () => {
- it('open&close', () => {
- })
- it('deprecatedOpen', () => {
- })
- })
-})
diff --git a/components/nav-menu/index.js b/components/nav-menu/index.js
deleted file mode 100644
index b5206f7d3..000000000
--- a/components/nav-menu/index.js
+++ /dev/null
@@ -1,4 +0,0 @@
-import NavMenu from './NavMenu'
-import './style/index'
-
-export default NavMenu
diff --git a/components/nav-menu/style/index.js b/components/nav-menu/style/index.js
deleted file mode 100644
index 14dd01250..000000000
--- a/components/nav-menu/style/index.js
+++ /dev/null
@@ -1,2 +0,0 @@
-import './index.scss'
-import '../../style/icon/index.scss'
diff --git a/components/nav-menu/style/index.scss b/components/nav-menu/style/index.scss
deleted file mode 100644
index 82ec563c7..000000000
--- a/components/nav-menu/style/index.scss
+++ /dev/null
@@ -1,178 +0,0 @@
-@import '@hi-ui/core-css/index.scss';
-
-.hi-navmenu {
- position: relative;
-
- &__list {
- border-bottom: 1px solid #e6e7e8;
- height: 48px;
- overflow: hidden;
- padding: 0 !important;
- margin: 0 !important;
-
- &--on {
- height: auto;
- }
-
- li {
- @extend .common-li;
- }
-
- &--vertical {
- height: auto;
- border-bottom: none;
-
- .hi-navmenu__item {
- margin-right: 0 !important;
- display: block !important;
- padding-left: 16px !important;
- padding-right: 16px !important;
-
- &.on {
- color: #4284f5;
- background: #ecf2fe;
-
- &::after {
- display: none;
- }
- }
- }
- }
- }
-
- &__toggle {
- display: none;
- box-sizing: border-box;
- background-color: #fff;
- position: absolute;
- right: 0;
- top: 12px;
- width: 24px;
- height: 24px;
- border-radius: 2px;
- border: 1px solid #d8d8d8;
- cursor: pointer;
-
- &--show {
- display: inline-block;
- }
-
- &::before {
- content: ' ';
- position: absolute;
- left: 50%;
- top: 50%;
- transform: translate3d(-50%, -50%, 0);
- border: 4px solid transparent;
- border-top: 4.4px solid #999;
- border-bottom: none;
- transition: transform 0.3s;
- transform-origin: 25% 12.5%;
- }
-
- &--on {
- &::before {
- transform: rotate(-180deg);
- }
- }
- }
-
- &__sublist {
- display: none;
-
- &--on {
- display: block;
- }
-
- li {
- @extend .common-li;
-
- &--on {
- &::after {
- display: none;
- }
- }
-
- &:not(:last-child) {
- &::before {
- position: absolute;
- right: -20px;
- top: 50%;
- transform: translate3d(0, -50%, 0);
- content: ' ';
- height: 10px;
- width: 1px;
- background-color: #d8d8d8;
- }
- }
- }
- }
-
- &__content {
- padding: 10px;
- }
-
- &__content-item {
- display: none;
-
- &--show {
- display: block;
- }
- }
-
- .common-li {
- font-size: 14px;
- color: #333;
- display: inline-block;
- line-height: 20px;
- padding: 14px 4px;
- margin-right: 40px;
- position: relative;
- cursor: pointer;
- margin-top: 0;
-
- img {
- vertical-align: middle;
- margin-right: 5px;
- }
-
- a {
- color: #333;
- }
-
- &:hover {
- color: #4284f5;
-
- a {
- color: #4284f5;
- }
- }
-
- &.on {
- color: #4284f5;
-
- &::after {
- position: absolute;
- left: 50%;
- bottom: 0;
- transform: translate3d(-50%, 0, 0);
- content: ' ';
- height: 2px;
- width: 32px;
- background-color: #4284f5;
- }
- }
- }
-
- &__item,
- &__sub {
- &--disabled {
- cursor: not-allowed;
- opacity: 0.5;
-
- &:hover {
- color: #333;
- }
- }
- }
-}
diff --git a/components/notice/style/index.scss b/components/notice/style/index.scss
index 7b479e781..964a30703 100755
--- a/components/notice/style/index.scss
+++ b/components/notice/style/index.scss
@@ -1,18 +1,18 @@
-@import '@hi-ui/core-css/index.scss';
@import '../../style/icon/index.scss';
+@import '../../core-css/index.scss';
.hi-notice__container {
position: absolute;
top: 20px;
right: 20px;
z-index: 1020;
- background-color: #fff;
+ background-color: use-color('white');
.hi-notice {
padding: 10px 12px;
margin-bottom: 10px;
- border: 1px solid #e7e7e7;
- background: #fff;
+ border: 1px solid use-color('gray-30');
+ background: use-color('white');
transition: 0.3s ease;
display: flex;
align-items: flex-start;
@@ -35,7 +35,7 @@
.hi-notice__content {
font-size: 12px;
- color: #666;
+ color: use-color('gray-80');
}
.hi-notice-enter {
diff --git a/components/notification/index.d.ts b/components/notification/index.d.ts
new file mode 100644
index 000000000..840330d6e
--- /dev/null
+++ b/components/notification/index.d.ts
@@ -0,0 +1,18 @@
+interface Options {
+ key: string
+ type?: 'info' | 'success' | 'error' | 'warning'
+ title?: string
+ content: string | JSX.Element
+ closeable?: boolean
+ duration?: number
+ confirmText?: string
+ onClose?: (e: MouseEvent) => void
+ onConfirm?: () => void
+}
+const OpenFun: (options: Options) => void
+const CloseFun: (key: string) => void
+declare class Notification extends React.Component {
+ static open = OpenFun
+ static close = CloseFun
+}
+export default Notification
diff --git a/components/notification/index.js b/components/notification/index.js
index fb15814ee..ed7785f25 100755
--- a/components/notification/index.js
+++ b/components/notification/index.js
@@ -1,20 +1,20 @@
import notice from '../notice'
import './style/index'
import React from 'react'
-import Button from '../button'
-import classNames from 'classnames'
+import { Button } from '../button/Button'
+import Icon from '../icon'
import _handleNotificate from './HandleNotification'
const iconMap = {
- success: 'chenggong',
- error: 'shibai',
- warning: 'jinggao',
- info: 'tishi'
+ success: 'check-circle',
+ danger: 'close-circle',
+ warning: 'exclamation-circle',
+ primary: 'info-circle'
}
export const handleNotificate = _handleNotificate
const notification = {
- close: key => {
+ close: (key) => {
notice.close('notification', key)
},
handleNotificate,
@@ -25,26 +25,26 @@ const notification = {
key = Math.random(),
duration,
closeable = true,
- type = 'info',
+ type = 'primary',
confirmText,
onConfirm,
onClose
}) => {
+ let _type = type === 'info' ? 'primary' : type
+ _type = type === 'error' ? 'danger' : _type
const NoticeContent = (
-
-
-
-
+
{content &&
{content}
}
{onConfirm && (
-
+
)
@@ -171,34 +181,40 @@ class Pagination extends Component {
return (
- {
- const val = e.target.value
- if (!val) {
- this.setState({
- jumpTo: val
- })
- return
- }
- if (/^\d+$/.test(val)) {
- const maxPage = this.calculatePage(total)
- const jumpTo = val < 1 ? 1 : (val > maxPage ? maxPage : val)
-
- this.setState({
- jumpTo
- })
- } else {
- this.setState({
- jumpTo: this.state.jumpTo
- })
- }
- }} />
+ {
+ const val = e.target.value
+ if (!val) {
+ this.setState({
+ jumpTo: val
+ })
+ return
+ }
+ if (/^\d+$/.test(val)) {
+ const maxPage = this.calculatePage(total)
+ const jumpTo = val < 1 ? 1 : val > maxPage ? maxPage : val
+
+ this.setState({
+ jumpTo
+ })
+ } else {
+ this.setState({
+ jumpTo: this.state.jumpTo
+ })
+ }
+ }}
+ />
)
}
gotoPage = e => {
const pageNum = parseInt(e.target.value)
- const setPageNum = (page) => {
+ const setPageNum = page => {
this.handleChange(page)
this.props.onJump && this.props.onJump(Number(page))
}
@@ -215,9 +231,7 @@ class Pagination extends Component {
renderPagers () {
const { max, total, prefixCls } = this.props
- const {
- current
- } = this.state
+ const { current } = this.state
const maxPage = this.calculatePage(total)
const prevPager = this.renderPrevPager() // 上一页
const nextPager = this.renderNextPager() // 下一页
@@ -226,11 +240,11 @@ class Pagination extends Component {
if (max * 2 + 1 + 2 >= maxPage) {
leftBuffer = 1
rightBuffer = maxPage
- } else if ((maxPage - current) <= max) {
+ } else if (maxPage - current <= max) {
rightBuffer = maxPage
leftBuffer = maxPage - 2 * max - 1
leftBuffer = leftBuffer <= 1 ? 1 : leftBuffer
- } else if ((current - max) <= 1) {
+ } else if (current - max <= 1) {
leftBuffer = 1
rightBuffer = 2 * max + leftBuffer + 1
rightBuffer = rightBuffer >= maxPage ? maxPage : rightBuffer
@@ -240,19 +254,29 @@ class Pagination extends Component {
}
if (leftBuffer !== 1) {
- pagers.push(this.renderPager(1, {active: current === 1}))
+ pagers.push(this.renderPager(1, { active: current === 1 }))
}
if (leftBuffer > 2) {
- pagers.push(this.renderPager('...', {className: `${prefixCls}__item-break`, itemRender: breakItemRender}))
+ pagers.push(
+ this.renderPager('...', {
+ className: `${prefixCls}__item-break`,
+ itemRender: breakItemRender
+ })
+ )
}
for (let index = leftBuffer; index <= rightBuffer; index++) {
- pagers.push(this.renderPager(index, {active: current === index}))
+ pagers.push(this.renderPager(index, { active: current === index }))
}
if (rightBuffer < maxPage - 1) {
- pagers.push(this.renderPager('...', {className: `${prefixCls}__item-break`, itemRender: breakItemRender}))
+ pagers.push(
+ this.renderPager('...', {
+ className: `${prefixCls}__item-break`,
+ itemRender: breakItemRender
+ })
+ )
}
if (rightBuffer !== maxPage) {
- pagers.push(this.renderPager(maxPage, {active: current === maxPage}))
+ pagers.push(this.renderPager(maxPage, { active: current === maxPage }))
}
pagers.push(nextPager)
@@ -262,13 +286,21 @@ class Pagination extends Component {
renderPrevPager () {
const { prefixCls } = this.props
const prevPage = this.prev()
- return this.renderPager(prevPage, {className: `${prefixCls}__item-prev`, disabled: prevPage < 1},
)
+ return this.renderPager(
+ prevPage,
+ { className: `${prefixCls}__item-prev`, disabled: prevPage < 1 },
+
+ )
}
renderNextPager () {
const { prefixCls } = this.props
const nextPage = this.next()
- return this.renderPager(nextPage, {className: `${prefixCls}__item-next`, disabled: nextPage < 1},
)
+ return this.renderPager(
+ nextPage,
+ { className: `${prefixCls}__item-next`, disabled: nextPage < 1 },
+
+ )
}
pagerIndex = 0
@@ -291,17 +323,26 @@ class Pagination extends Component {
)
}
- renderNormal () { // 标准分页
- const { prefixCls, showTotal, total, localeDatas: { pagination: { total: i18nTotal } } } = this.props
+ renderNormal () {
+ // 标准分页
+ const {
+ prefixCls,
+ showTotal,
+ total,
+ localeDatas: {
+ pagination: { total: i18nTotal }
+ }
+ } = this.props
return (
- {
- showTotal &&
+ {showTotal && (
- {i18nTotal[0]}{total}{i18nTotal[1]}
+ {i18nTotal[0]}
+ {total}
+ {i18nTotal[1]}
- }
+ )}
{this.renderPageSizes()}
{this.renderPagers()}
{this.renderJumper()}
@@ -309,7 +350,8 @@ class Pagination extends Component {
)
}
- renderSimple () { // 简单分页
+ renderSimple () {
+ // 简单分页
const {
total,
prefixCls,
@@ -324,18 +366,19 @@ class Pagination extends Component {
{this.renderJumperInput()}
{i18nSimple[1]}
/
- {i18nSimple[2]}{maxPage}{i18nSimple[3]},
- {total} {i18nSimple[4]}
+ {i18nSimple[2]}
+ {maxPage}
+ {i18nSimple[3]},
+
+ {total} {i18nSimple[4]}
+
)
}
- renderPn () { // 上一页下一页
- const {
- prefixCls,
- total,
- showJumper
- } = this.props
+ renderPn () {
+ // 上一页下一页
+ const { prefixCls, total, showJumper } = this.props
const maxPage = this.calculatePage(total)
const prevPager = this.renderPrevPager()
const nextPager = this.renderNextPager()
@@ -343,30 +386,21 @@ class Pagination extends Component {
return (
{prevPager}
- {
- showJumper &&
+ {showJumper && (
- {this.renderJumperInput()}
- /
- {maxPage}
+ {this.renderJumperInput()}/
+ {maxPage}
- }
+ )}
{nextPager}
)
}
render () {
- const {
- autoHide,
- total,
- type,
- prefixCls,
- className,
- theme
- } = this.props
+ const { autoHide, total, type, prefixCls, className, theme } = this.props
const maxPage = this.calculatePage(total)
- if (maxPage === 0 || (autoHide && (maxPage === 1))) {
+ if (maxPage === 0 || (autoHide && maxPage === 1)) {
return null
}
let children
@@ -385,7 +419,10 @@ class Pagination extends Component {
}
return (
-
+
{children}
)
@@ -422,4 +459,4 @@ Pagination.defaultProps = {
}
export default Provider(Pagination)
-export {Pagination}
+export { Pagination }
diff --git a/components/pagination/index.d.ts b/components/pagination/index.d.ts
new file mode 100644
index 000000000..a6bac4d13
--- /dev/null
+++ b/components/pagination/index.d.ts
@@ -0,0 +1,16 @@
+export interface PaginationProps {
+ type?: 'default' | 'simple' | 'shrink'
+ defaultCurrent?: number
+ current?: number
+ max?: number
+ pageSize?: number
+ total: number
+ pageSizeOptions?: number[]
+ autoHide?: boolean
+ showJumper?: boolean
+ onJump?: (current: number) => void
+ onChange?: (currentPage: number, prevPage: number, pageSize: number) => void
+ onPageSizeChange?: (changeSize: number, currentPage: number) => void
+}
+declare const Pagination: React.ComponentType
+export default Pagination
diff --git a/components/pagination/style/index.scss b/components/pagination/style/index.scss
index 705dfa8bb..22f651314 100644
--- a/components/pagination/style/index.scss
+++ b/components/pagination/style/index.scss
@@ -1,7 +1,7 @@
-@import '@hi-ui/core-css/index.scss';
+@import '../../core-css/index.scss';
// Define component colors
-@mixin hi-pagination-style($color: get-color($palette-primary, 'hiui-blue')) {
+@mixin hi-pagination-style($color: use-color('primary')) {
.hi-pagination {
&__item {
&:not(.hi-pagination__item--disabled) {
@@ -35,11 +35,9 @@
align-items: center;
user-select: none;
- @include component-reset();
-
&__text {
line-height: 32px;
- color: $gray-darker;
+ color: use-color('gray-80');
}
&__total {
@@ -59,20 +57,20 @@
margin: 0 $spacer-1;
.hi-select__input {
- border: 1px solid $gray-light;
+ border: 1px solid use-color('gray-20');
}
}
&__jumper-input {
display: inline-block;
margin: 0 $spacer-5 0 $spacer-2;
- color: $gray-darker;
+ color: use-color('gray-80');
.hi-input {
width: 64px;
&__inner {
- border: 1px solid $gray-light;
+ border: 1px solid use-color('gray-20');
}
&__text {
@@ -103,13 +101,13 @@
min-width: 32px;
padding: 0 4px;
height: 32px;
- border: 1px solid $gray-light;
+ border: 1px solid use-color('gray-20');
border-radius: 2px;
font-size: $font-size-normal;
line-height: 30px;
text-decoration: none;
text-align: center;
- color: $gray-darker;
+ color: use-color('gray-80');
cursor: pointer;
&:hover {
@@ -126,12 +124,12 @@
&--disabled {
> span {
- border: 1px solid $gray-light;
- color: $gray-light;
+ border: 1px solid use-color('gray-20');
+ color: use-color('gray-20');
cursor: default;
&:hover {
- border: 1px solid $gray-light;
+ border: 1px solid use-color('gray-20');
}
}
}
@@ -145,9 +143,3 @@
// Component colors
@include hi-pagination-style();
-
-@each $key, $value in $theme-colors {
- .theme__#{$key} {
- @include hi-pagination-style($value);
- }
-}
diff --git a/components/panel/index.js b/components/panel/index.js
deleted file mode 100755
index f3f1a8df7..000000000
--- a/components/panel/index.js
+++ /dev/null
@@ -1,40 +0,0 @@
-import React, { Component } from 'react'
-import PropTypes from 'prop-types'
-import classNames from 'classnames'
-import './style/index'
-
-class Panel extends Component {
- static propTypes = {
- title: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
- footer: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
- icon: PropTypes.string,
- type: PropTypes.string
- }
- static defaultProps = {
- prefixCls: 'hi-panel',
- type: 'info'
- }
- render () {
- let classnames = classNames(this.props.prefixCls, this.props.type)
- const { title, footer, children, icon } = this.props
-
- return (
-
-
- {
- icon &&
- }
- {title}
-
-
{children}
- {
- typeof footer !== 'undefined' && (
-
{footer}
- )
- }
-
- )
- }
-}
-
-export default Panel
diff --git a/components/panel/style/index.js b/components/panel/style/index.js
deleted file mode 100644
index 14dd01250..000000000
--- a/components/panel/style/index.js
+++ /dev/null
@@ -1,2 +0,0 @@
-import './index.scss'
-import '../../style/icon/index.scss'
diff --git a/components/panel/style/index.scss b/components/panel/style/index.scss
deleted file mode 100755
index ea71b4d3a..000000000
--- a/components/panel/style/index.scss
+++ /dev/null
@@ -1,31 +0,0 @@
-.hi-panel {
- padding: 10px;
- font-size: 0;
- box-shadow: 0 5px 15px 0 rgba(0, 0, 0, 0.1);
-
- .panel-title {
- display: block;
- height: 40px;
- line-height: 40px;
- padding: 0 16px;
- font-size: 16px;
-
- .hi-icon {
- margin-right: 5px;
- }
- }
-
- .panel-content {
- display: block;
- font-size: 14px;
- padding: 16px;
- border-top: 1px solid #e6e7e8;
- }
-
- .panel-footer {
- display: block;
- font-size: 14px;
- padding: 10px 16px;
- border-top: 1px solid #e6e7e8;
- }
-}
diff --git a/components/popover/__test__/index.test.js b/components/popover/__test__/index.test.js
deleted file mode 100644
index f07883251..000000000
--- a/components/popover/__test__/index.test.js
+++ /dev/null
@@ -1,53 +0,0 @@
-import React from 'react'
-import { mount } from 'enzyme'
-import Popover from '../'
-import Button from '../../button'
-
-const topClickTarget = mount(
-
-)
-const focusRightTarget = mount(
-
-)
-const leftTarget = mount(
-
-
-
-)
-const bottomTarget = mount(
-
-
-
-)
-
-describe('Tooltip', () => {
- it('触发方式测试通过', () => {
- expect(document.querySelectorAll('.hi-popover__popper')).toHaveLength(0)
-
- topClickTarget.find('.hi-btn').getDOMNode().click()
- expect(document.querySelectorAll('.hi-popover__popper')).toHaveLength(1)
-
- focusRightTarget.find('.hi-btn').getDOMNode().focus()
- expect(document.querySelectorAll('.hi-popover__popper')).toHaveLength(2)
- // focusRightTarget.find('.hi-btn').simulate('mouseenter')
- })
-
- it('显示的位置测试通过', () => {
- topClickTarget.find('.hi-btn').getDOMNode().click()
- leftTarget.find('.hi-btn').getDOMNode().click()
- bottomTarget.find('.hi-btn').getDOMNode().click()
-
- expect(document.querySelectorAll('.hi-popper__content--top')).toHaveLength(1)
- expect(document.querySelectorAll('.hi-popper__content--right')).toHaveLength(1)
- expect(document.querySelectorAll('.hi-popper__content--left')).toHaveLength(1)
- expect(document.querySelectorAll('.hi-popper__content--bottom')).toHaveLength(1)
- })
-})
diff --git a/components/popover/index.d.ts b/components/popover/index.d.ts
new file mode 100644
index 000000000..124a86acd
--- /dev/null
+++ b/components/popover/index.d.ts
@@ -0,0 +1,9 @@
+interface Props {
+ title?: string | JSX.Element
+ content: string | JSX.Element
+ placement?: 'top' | 'right' | 'bottom' | 'left'
+ trigger?: 'click' | 'focus' | 'hover'
+ visible?: boolean
+}
+declare const Popover: React.ComponentType
+export default Popover
diff --git a/components/popover/index.js b/components/popover/index.js
index a41c61a02..a22227dab 100644
--- a/components/popover/index.js
+++ b/components/popover/index.js
@@ -5,7 +5,6 @@ import classNames from 'classnames'
import Popper from '../popper'
import './style/index'
export default class Popover extends Component {
- unbindHover = true
static defaultProps = {
trigger: 'click',
placement: 'top',
@@ -18,7 +17,7 @@ export default class Popover extends Component {
content: PropTypes.oneOfType([PropTypes.node, PropTypes.string]),
width: PropTypes.string
}
- constructor (props) {
+ constructor(props) {
super(props)
this.state = {
showPopper: false
@@ -29,35 +28,39 @@ export default class Popover extends Component {
this.delayHidePopperTimer = null
}
- showPopper () {
+ showPopper() {
this.setState({
showPopper: true
})
}
- hidePopper () {
+ hidePopper() {
this.setState({
showPopper: false
})
}
- delayHidePopper (e) {
+ delayHidePopper(e) {
this.delayHidePopperTimer = setTimeout(() => {
if (this.eventTarget !== e.target && this.isInPopover()) return
this.hidePopper()
}, 200)
}
- delayShowPopper (e) {
+ delayShowPopper(e) {
this.delayShowPopperTimer = setTimeout(() => {
this.showPopper()
}, 200)
}
- isInPopover () {
+ isInPopover() {
const popper = this.popperRef.current
const referenceRef = ReactDOM.findDOMNode(this.referenceRef)
- const bool = !this.element || this.element.contains(this.eventTarget) ||
- !referenceRef || referenceRef.contains(this.eventTarget) ||
- !popper || popper.contains(this.eventTarget)
+ const bool =
+ !this.element ||
+ this.element.contains(this.eventTarget) ||
+ !referenceRef ||
+ referenceRef.contains(this.eventTarget) ||
+ !popper ||
+ popper.contains(this.eventTarget)
this.eventTarget = null
return bool
}
@@ -70,14 +73,14 @@ export default class Popover extends Component {
}
}
- handlePopperMouseOut = () => {
+ handlePopperMouseLeave = () => {
const { trigger } = this.props
if (trigger === 'hover') {
this.hidePopper()
}
}
- componentDidMount () {
+ componentDidMount() {
const { trigger } = this.props
this.element = ReactDOM.findDOMNode(this)
@@ -101,11 +104,11 @@ export default class Popover extends Component {
this.hidePopper()
})
} else if (trigger === 'hover') {
- referenceRef.addEventListener('mouseenter', e => {
+ referenceRef.addEventListener('mouseenter', (e) => {
this.eventTarget = e.target
this.delayShowPopper(e)
})
- referenceRef.addEventListener('mouseleave', e => {
+ referenceRef.addEventListener('mouseleave', (e) => {
this.delayHidePopper(e)
clearTimeout(this.delayShowPopperTimer)
})
@@ -115,14 +118,23 @@ export default class Popover extends Component {
}
}
- render () {
- const { style, className, title, content, placement, width, visible } = this.props
- const {
- showPopper
- } = this.state
+ render() {
+ const { style, className, title, content, placement, width, visible, overlayClassName } = this.props
+ const { showPopper } = this.state
return (
- { this.popoverContainer = node }}>
- { React.cloneElement(React.Children.only(this.props.children), { ref: (el) => { this.referenceRef = el }, tabIndex: '0' }) }
+
{
+ this.popoverContainer = node
+ }}
+ >
+ {React.cloneElement(React.Children.only(this.props.children), {
+ ref: (el) => {
+ this.referenceRef = el
+ },
+ tabIndex: '0'
+ })}
- { title &&
{title}
}
-
- { content }
-
+ {title &&
{title}
}
+
{content}
diff --git a/components/popover/style/index.scss b/components/popover/style/index.scss
index e90053669..05f15b8df 100644
--- a/components/popover/style/index.scss
+++ b/components/popover/style/index.scss
@@ -1,3 +1,4 @@
+@import '../../core-css/index.scss';
$popper-gap: 10px !default;
$popper-arrow-gap: -5px !default;
@@ -7,7 +8,7 @@ $popper-arrow-gap: -5px !default;
}
.hi-popover-base {
- background: #fff;
+ background: use-color('white');
padding: 0 5px;
white-space: nowrap;
transition: all 0.5s;
@@ -19,8 +20,8 @@ $popper-arrow-gap: -5px !default;
position: absolute;
width: 10px;
height: 10px;
- border: 1px solid #e8e8e8;
- background: #fff;
+ border: 1px solid use-color('gray-20');
+ background: use-color('white');
box-sizing: border-box;
}
@@ -31,7 +32,7 @@ $popper-arrow-gap: -5px !default;
.hi-popover__title {
padding: 5px 15px;
color: #000;
- border-bottom: 1px solid #e8e8e8;
+ border-bottom: 1px solid use-color('gray-20');
}
.hi-popover__content {
diff --git a/components/popper/Overlay.js b/components/popper/Overlay.js
new file mode 100644
index 000000000..f05eb680b
--- /dev/null
+++ b/components/popper/Overlay.js
@@ -0,0 +1,219 @@
+import React, { useState, useRef, useEffect, useCallback } from 'react'
+import PropTypes from 'prop-types'
+import classNames from 'classnames'
+import _ from 'lodash'
+import PopperJS from './utils/popper'
+import { getOffset } from './utils/positionUtils'
+import useClickOutside from './utils/useClickOutside'
+import './style/index'
+
+const {
+ isFixed,
+ setupEventListeners,
+ removeEventListeners,
+ setStyle,
+ getStyleComputedProperty,
+ isBody
+} = new PopperJS()
+
+const Overlay = (props) => {
+ const {
+ show,
+ attachEle,
+ children,
+ className,
+ height,
+ zIndex,
+ onMouseOver,
+ onMouseOut,
+ onMouseEnter,
+ onMouseLeave,
+ onClickOutside,
+ overlayClassName
+ } = props
+ const [isAddevent, setIsAddevent] = useState(false)
+ const [state, setState] = useState({
+ offset: undefined,
+ popperHeight: undefined,
+ popperWidth: undefined,
+ cacheContainerPosition: 'static',
+ popperRef: undefined
+ })
+
+ let popperHeight
+ let popperWidth
+ const staticPopperRef = useRef()
+ const offsetData = useRef()
+ let popperContainerRef
+
+ if (onClickOutside) {
+ popperContainerRef = useClickOutside(
+ (e) => {
+ onClickOutside && onClickOutside(e)
+ },
+ undefined,
+ 'click',
+ attachEle
+ )
+ }
+
+ const scrollCallBack = useCallback(() => {
+ const offset = getOffset(props, state)
+ offsetData.current = offset
+ if (staticPopperRef) {
+ setState(
+ Object.assign({}, state, {
+ popperRef: staticPopperRef.current
+ })
+ )
+ }
+ }, [props, state])
+
+ useEffect(() => {
+ const { attachEle, container, show } = props
+ const { cacheContainerPosition } = state
+ const offset = getOffset(props, state)
+ offsetData.current = offset
+ if (staticPopperRef) {
+ setState(
+ Object.assign({}, state, {
+ popperRef: staticPopperRef.current
+ })
+ )
+ }
+ if (!show) {
+ // 删除滚动
+ attachEle && isAddevent && removeEventListeners(attachEle)
+ // 判断该元素中是否含有popper如果有popper在显示 就不要删除定位
+ setTimeout(() => {
+ if (container.querySelectorAll('.hi-popper__container').length === 0) {
+ container && !isBody(container) && isAddevent && setStyle(container, { position: cacheContainerPosition })
+ }
+ }, 0)
+ setIsAddevent(false)
+ setState(Object.assign({}, state, { offset: undefined }))
+ }
+ }, [props.show])
+
+ // update
+ useEffect(() => {
+ const { attachEle, children, container, show } = props
+ if (!(attachEle && show && children)) return
+
+ const { cacheContainerPosition, popperRef } = state
+ if (show && !isAddevent) {
+ !isAddevent && setupEventListeners(attachEle, scrollCallBack)
+ setIsAddevent(true)
+ }
+ // 如果在一个固定定位的元素里面的话;更改计算方式
+ if (isFixed(attachEle) && !isBody(container)) {
+ cacheContainerPosition === 'static' && setStyle(container, { position: 'relative' })
+ }
+ if (!popperRef) {
+ setState(
+ Object.assign({}, state, {
+ popperRef: staticPopperRef.current,
+ popperHeight: staticPopperRef.current.clientHeight,
+ popperWidth: staticPopperRef.current.clientWidth
+ })
+ )
+ }
+ })
+
+ useEffect(() => {
+ const offset = getOffset(props, state)
+ offsetData.current = offset
+
+ state.popperRef &&
+ setState(
+ Object.assign({}, state, {
+ offset: offset
+ })
+ )
+ }, [state.popperRef])
+ // DidMount
+ useEffect(() => {
+ const { container } = props
+ setState(
+ Object.assign({}, state, {
+ cacheContainerPosition: container ? getStyleComputedProperty(container, 'position') : 'static'
+ })
+ )
+ }, [])
+
+ if (!(attachEle && show && children)) return null
+
+ const { offset = getOffset(props, state) } = state
+ const width = offset.width
+ const left = offset.left + 'px'
+ const top = offset.top + 'px'
+
+ return (
+
+
{
+ staticPopperRef.current = node
+ }}
+ className={classNames(className, 'hi-popper__content', `hi-popper__content--${offset.placement}`, {
+ 'hi-popper__content--hide': popperHeight === 0 || popperWidth === 0
+ })}
+ style={{ width, height }}
+ onMouseOut={onMouseOut}
+ onMouseOver={onMouseOver}
+ onMouseLeave={onMouseLeave}
+ onMouseEnter={onMouseEnter}
+ >
+ {children}
+
+
+ )
+}
+Overlay.defaultProps = {
+ show: false,
+ topGap: 0,
+ leftGap: 0,
+ zIndex: 1060,
+ placement: 'bottom-start'
+}
+
+Overlay.propTypes = {
+ width: PropTypes.oneOfType([PropTypes.string, PropTypes.bool, PropTypes.number]), // 为false时不设置
+ height: PropTypes.number,
+ className: PropTypes.string,
+ show: PropTypes.bool,
+ topGap: PropTypes.number,
+ leftGap: PropTypes.number,
+ zIndex: PropTypes.number,
+ placement: PropTypes.oneOf([
+ 'auto', // 会计算最合适的位置
+ 'bottom',
+ 'bottom-start',
+ 'bottom-end',
+ 'top',
+ 'top-start',
+ 'top-end',
+ 'left',
+ 'left-start',
+ 'left-end',
+ 'right',
+ 'right-start',
+ 'right-end',
+ 'top-bottom-start',
+ 'top-bottom',
+ 'left-right',
+ 'left-right-start'
+ ]),
+ onMouseOver: PropTypes.func,
+ onMouseOut: PropTypes.func,
+ onMouseEnter: PropTypes.func,
+ onMouseLeave: PropTypes.func,
+ container: PropTypes.any,
+ preventOverflow: PropTypes.bool // 防止溢出 top bottom
+}
+export default Overlay
diff --git a/components/popper/Portal.js b/components/popper/Portal.js
new file mode 100644
index 000000000..48e9be28b
--- /dev/null
+++ b/components/popper/Portal.js
@@ -0,0 +1,27 @@
+/**
+ * 指定挂载点
+ */
+/* eslint-disable-next-line */
+import React from 'react'
+import PropTypes from 'prop-types'
+import ReactDOM from 'react-dom'
+import useWaitForDOMRef from './utils/useWaitForDOMRef'
+const getNodeName = (element) => {
+ return element ? (element.nodeName || '').toLowerCase() : null
+}
+const Portal = ({ container, children, onRendered }) => {
+ let resolvedContainer = useWaitForDOMRef(container, onRendered)
+ resolvedContainer = ['html', 'body', '#document'].includes(getNodeName(resolvedContainer))
+ ? document.body
+ : resolvedContainer
+
+ return children && resolvedContainer ? <>{ReactDOM.createPortal(children, resolvedContainer)}> : null
+}
+
+Portal.displayName = 'Portal'
+Portal.propTypes = {
+ container: PropTypes.any,
+ onRendered: PropTypes.func
+}
+
+export default Portal
diff --git a/components/popper/index.js b/components/popper/index.js
index 572f49800..8ce94d15c 100644
--- a/components/popper/index.js
+++ b/components/popper/index.js
@@ -1,210 +1,48 @@
-import React, { Component } from 'react'
-import { render } from 'react-dom'
-import PropTypes from 'prop-types'
-import classNames from 'classnames'
-import _ from 'lodash'
-import './style/index'
-
-export default class Popper extends Component {
- container = undefined
- popperHeight = undefined
-
- static propTypes = {
- width: PropTypes.oneOfType([PropTypes.string, PropTypes.bool, PropTypes.number]), // 为false时不设置
- height: PropTypes.number,
- className: PropTypes.string,
- show: PropTypes.bool,
- topGap: PropTypes.number,
- leftGap: PropTypes.number,
- zIndex: PropTypes.number,
- placement: PropTypes.oneOf(['bottom', 'bottom-start', 'top', 'top-start', 'top-end', 'left', 'right', 'right-start', 'top-bottom-start', 'top-bottom', 'bottom-end']),
- onMouseOver: PropTypes.func,
- onMouseOut: PropTypes.func,
- onMouseEnter: PropTypes.func,
- onMouseLeave: PropTypes.func
- }
-
- static defaultProps = {
- show: false,
- topGap: 2,
- leftGap: 2,
- zIndex: 1060,
- placement: 'bottom-start'
- }
-
- componentDidUpdate (prevProps) {
- if (!_.isEqual(prevProps.children.props, this.props.children.props)) {
- this.forceUpdate()
- }
- if (prevProps.show !== this.props.show || this.props.show) {
- render(this.renderChildren(), this.container)
- }
- }
-
- componentDidMount () {
- this.getContainer()
- if (this.props.show) {
- render(this.renderChildren(), this.container)
- }
- }
-
- componentWillUnmount () {
- document.body.removeChild(this.container)
- }
-
- getOffset () {
- let {
- attachEle,
- topGap,
- leftGap,
- width
- } = this.props
- if (!attachEle) return
- const rect = attachEle.getBoundingClientRect()
- let top = rect.top + (document.documentElement.scrollTop || document.body.scrollTop)
- let left = rect.left + (document.documentElement.scrollLeft || document.body.scrollLeft)
- width = width === false ? undefined : (width === undefined ? rect.width : width)
- let placement = this.getPlacement(rect)
- switch (placement) {
- case 'bottom':
- top = top + topGap + rect.height
- left = left + leftGap + rect.width / 2
- break
- case 'bottom-start':
- top = top + topGap + rect.height
- left = left + leftGap
- break
- case 'bottom-end':
- top = top + topGap + rect.height
- left = left + leftGap - width + rect.width
- break
- case 'top':
- top = top - topGap
- left = left + leftGap + rect.width / 2
- break
- case 'top-start':
- top = top - topGap
- left = left + leftGap
- break
-
- case 'top-end':
- top = top - topGap
- left = left + leftGap - width + rect.width
- break
-
- case 'left':
- top = top + rect.height / 2
- left = left - leftGap
- break
+import React, { useState, useEffect } from 'react'
+import { CSSTransition } from 'react-transition-group'
+import Portal from './Portal'
+import Overlay from './Overlay'
+import useClickOutside from './utils/useClickOutside'
- case 'right':
- top = top + rect.height / 2
- left = left + rect.width + leftGap
- break
- case 'right-start':
- top = top + topGap
- left = left + rect.width + leftGap
- break
- }
-
- return {
- width,
- top,
- left,
- placement
- }
- }
-
- getPlacement (attachEleRect) {
- let {
- attachEle,
- placement,
- height
- } = this.props
- if (!attachEle) return
- const bodyHeight = document.documentElement.clientHeight || document.body.clientHeight
- let poperTop = attachEleRect.top + attachEleRect.height
- const caclPlacement = (bottomPlacement, topPlacement) => { // 计算popper在元素上面或下面
- placement = bottomPlacement
- this.popperHeight === undefined && (this.popperHeight = 0) // 自动探测边界,第一次时需设置为不可见,否则会闪跳,用来设置class hi-popper__content--hide
- if (this.popperRef || height) { // 元素已挂载到dom且当前popper处于显示状态
- if (height) {
- this.popperHeight = height
- } else if (this.popperRef.clientHeight && this.popperHeight !== this.popperRef.clientHeight) {
- this.popperHeight = this.popperRef.clientHeight
- }
- poperTop += this.popperHeight
- if (poperTop >= bodyHeight) {
- placement = topPlacement
- }
- } else { // popper尚未挂载到dom,拿不到高度
- setTimeout(() => render(this.renderChildren(), this.container), 0)
- }
- }
-
- if (placement === 'top-bottom-start') {
- caclPlacement('bottom-start', 'top-start')
- } else if (placement === 'top-bottom') {
- caclPlacement('bottom', 'top')
- }
-
- return placement
- }
-
- renderChildren () {
- let {
- attachEle,
- children,
- className,
- show,
- height,
- zIndex,
- onMouseOver,
- onMouseOut,
- onMouseEnter,
- onMouseLeave
- } = this.props
- if (!attachEle) return
- const offset = this.getOffset()
- let width = offset.width
- let left = offset.left + 'px'
- let top = offset.top + 'px'
+import './style/index'
- return (
-
{
+ const { show, attachEle, setOverlayContainer } = props
+ const [staticShow, setStaticShow] = useState(show)
+ const [transitionShow, setTransitionShow] = useState(show)
+ const [container, setContainer] = useState(
+ setOverlayContainer ? setOverlayContainer(attachEle) : props.container || document.body
+ )
+
+ useEffect(() => {
+ // const _container = attachEle ? getScrollParent(attachEle) : document.body
+ setContainer(setOverlayContainer ? setOverlayContainer(attachEle) : props.container || document.body)
+ setTransitionShow(show)
+ show && setStaticShow(true)
+ }, [show, attachEle])
+ return (
+
+
{
+ setStaticShow(false)
+ }}
>
- {
- this.popperRef = node
- }}
- className={classNames(className, 'hi-popper__content', `hi-popper__content--${offset.placement}`, {'hi-popper__content--hide': this.popperHeight === 0})}
- style={{width, height}}
- onMouseOut={onMouseOut}
- onMouseOver={onMouseOver}
- onMouseLeave={onMouseLeave}
- onMouseEnter={onMouseEnter}
- >
- { children }
-
-
- )
- }
-
- getContainer () {
- const container = document.createElement('div')
- container.style.position = 'absolute'
- container.style.top = '0'
- container.style.left = '0'
- container.style.width = '100%'
-
- document.body.appendChild(container)
-
- this.container = container
- }
-
- render () {
- return null
- }
+
+
+
+
+
+ )
}
+export { Portal, useClickOutside }
+export default Popper
diff --git a/components/popper/style/index.scss b/components/popper/style/index.scss
index ee888811d..f45f8f7e3 100644
--- a/components/popper/style/index.scss
+++ b/components/popper/style/index.scss
@@ -1,3 +1,4 @@
+$AnimationClassName :'hi-popper_transition' !default;
.hi-popper__container {
position: absolute;
@@ -47,3 +48,33 @@
}
}
}
+
+.#{$AnimationClassName} {
+ &-enter {
+ opacity: 0;
+ transform: scale(0.9);
+ }
+
+ &-enter-active {
+ opacity: 1;
+ transform: translateX(0);
+ transition: opacity 300ms, transform 300ms;
+ }
+
+ &-exit {
+ opacity: 0;
+ transform: translateX(100%);
+ transition: opacity 300ms, transform 300ms;
+ }
+
+ &-exit-done {
+ opacity: 0;
+ transform: translateX(100%);
+ }
+
+ &-exit-active {
+ opacity: 0;
+ transform: scale(0.9);
+ transition: opacity 300ms, transform 300ms;
+ }
+}
diff --git a/components/popper/utils/popper.js b/components/popper/utils/popper.js
new file mode 100644
index 000000000..b3b9f8273
--- /dev/null
+++ b/components/popper/utils/popper.js
@@ -0,0 +1,372 @@
+const root = window
+const boundariesElement = 'viewport'
+export default class Popper {
+ /**
+ * 获得给定元素的外围尺寸(offset大小 + 外边距)
+ * @function
+ * @ignore
+ * @argument {Element} element 要检测的元素
+ * @returns {Object} 包含宽高信息的对象
+ */
+ getOuterSizes = element => {
+ // 注:这里会访问 DOM
+ let _display = element.style.display
+ let _visibility = element.style.visibility
+ element.style.display = 'block'
+ element.style.visibility = 'hidden'
+
+ // original method
+ // 原始方法
+ var styles = root.getComputedStyle(element) // 获取计算后的样式
+ var x = parseFloat(styles.marginTop) + parseFloat(styles.marginBottom) // 上下边距
+ var y = parseFloat(styles.marginLeft) + parseFloat(styles.marginRight) // 左右边距
+ var result = {
+ width: element.offsetWidth + y,
+ height: element.offsetHeight + x
+ }
+
+ // 重置元素样式
+ element.style.display = _display
+ element.style.visibility = _visibility
+ return result
+ }
+ /**
+ * 获得 popper 的偏移量
+ * @method
+ * @memberof Popper
+ * @access private
+ * @param {Element} popper - popper 元素
+ * @param {Element} reference - 相关元素(popper 将根据它定位)
+ * @returns {Object} 包含将应用于 popper 的位移信息的对象
+ */
+ getOffsets = (popper, reference, position, placement) => {
+ const {
+ getOffsetRectRelativeToCustomParent,
+ getOuterSizes,
+ getOffsetParent
+ } = this
+ // 获取前缀
+ placement = placement.split('-')[0]
+ var popperOffsets = {}
+
+ // 设置 position
+ popperOffsets.position = position
+ // 判断父元素是否固定定位
+ var isParentFixed = popperOffsets.position === 'fixed'
+
+ //
+ // 获取相关元素的位置
+ //
+ var referenceOffsets = getOffsetRectRelativeToCustomParent(
+ reference,
+ getOffsetParent(popper),
+ isParentFixed
+ )
+
+ //
+ // 获取 popper 的大小
+ //
+ var popperRect = getOuterSizes(popper)
+ //
+ // 计算 popper 的偏移
+ //
+ // 根据 popper 放置位置的不同,我们用不同的方法计算
+ if (['right', 'left'].indexOf(placement) !== -1) {
+ // 如果在水平方向,应当和相关元素垂直居中对齐
+ // top 应当为相关元素的 top 加上二者的高度差的一半,这样才能保证垂直居中对齐
+ popperOffsets.top =
+ referenceOffsets.top +
+ referenceOffsets.height / 2 -
+ popperRect.height / 2
+ if (placement === 'left') {
+ // 如果在左边,则 left 应为相关元素的 left 减去 popper 的宽度
+ popperOffsets.left = referenceOffsets.left - popperRect.width
+ } else {
+ // 如果在右边,则 left 应为相关元素的 right
+ popperOffsets.left = referenceOffsets.right
+ }
+ } else {
+ // 如果在垂直方向,应当和相关元素水平居中对齐
+ // left 应当为相关元素的 left 加上二者的宽度差的一半
+ popperOffsets.left =
+ referenceOffsets.left +
+ referenceOffsets.width / 2 -
+ popperRect.width / 2
+ if (placement === 'top') {
+ // 如果在上边,则 top 应当为相关元素的 top 减去 popper 的高度
+ popperOffsets.top = referenceOffsets.top - popperRect.height
+ } else {
+ // 如果在下边,则 top 应当为 相关元素的 bottom
+ popperOffsets.top = referenceOffsets.bottom
+ }
+ }
+
+ // 给 popperOffsets 对象增加宽度和高度值
+ popperOffsets.width = popperRect.width
+ popperOffsets.height = popperRect.height
+
+ return {
+ popper: popperOffsets, // popper 的相关信息
+ reference: referenceOffsets // 相关元素的相关信息
+ }
+ }
+
+ /**
+ * 为给定的 popper 设定样式
+ * @function
+ * @ignore
+ * @argument {Element} element - 要设定样式的元素
+ * @argument {Object} styles - 包含样式信息的对象
+ */
+ setStyle = (element, styles) => {
+ function isNumeric (n) {
+ // 是否是数字
+ return n !== '' && !isNaN(parseFloat(n)) && isFinite(n)
+ }
+ Object.keys(styles).forEach(function (prop) {
+ var unit = ''
+ // 为如下的属性增加单位
+ if (
+ ['width', 'height', 'top', 'right', 'bottom', 'left'].indexOf(prop) !==
+ -1 &&
+ isNumeric(styles[prop])
+ ) {
+ unit = 'px'
+ }
+ element.style[prop] = styles[prop] + unit
+ })
+ }
+ /**
+ * 初始化更新 popper 位置时用到的事件监听器
+ * @method
+ * @memberof Popper
+ * @access private
+ */
+ setupEventListeners = (element, callback) => {
+ // 1 DOM access here
+ // 注:这里会访问 DOM,原作者回复我说,这是他用来记录哪里访问到了 DOM
+ // this.state.updateBound = this.update.bind(this)
+ // 浏览器窗口改变的时候更新边界
+ this.scrollCallback = () => {
+ callback(target)
+ }
+ root.addEventListener('resize', this.scrollCallback)
+ // 如果边界元素是窗口,就不需要监听滚动事件
+ if (boundariesElement !== 'window') {
+ var target = this.getScrollParent(element) // 获取相关元素可滚动的父级
+ // 这里可能是 `body` 或 `documentElement`(Firefox上),等价于要监听根元素
+ if (
+ target === root.document.body ||
+ target === root.document.documentElement
+ ) {
+ target = root
+ }
+ // 监听滚动事件
+ target.addEventListener('scroll', this.scrollCallback)
+ }
+ }
+ getNodeName (element) {
+ return element ? (element.nodeName || '').toLowerCase() : null
+ }
+ isBody = element => {
+ const { getNodeName } = this
+ return element
+ ? ['html', 'body', '#document'].includes(getNodeName(element))
+ : true
+ }
+ /**
+ * 返回给定元素用来计算滚动的父元素
+ * @function
+ * @ignore
+ * @argument {Element} element
+ * @returns {Element} scroll parent
+ */
+ getScrollParent = element => {
+ // return document.body
+
+ const { getStyleComputedProperty, getScrollParent, getNodeName } = this
+ var parent = element.parentNode
+ if (['html', 'body', '#document'].includes(getNodeName(element))) {
+ // $FlowFixMe: assume body is always available
+ return document.documentElement || document.body
+ }
+ if (!parent) {
+ // 没有父级
+ return element
+ }
+
+ if (parent === root.document) {
+ // Firefox 会将 scrollTop的判断放置的 `documentElement` 而非 `body` 上
+ // 我们将判断二者谁大于0来返回正确的元素
+ if (root.document.body.scrollTop) {
+ return root.document.body
+ } else {
+ return root.document.documentElement
+ }
+ }
+
+ // Firefox 要求我们也要检查 `-x` 以及 `-y`
+ if (
+ ['scroll', 'auto'].indexOf(
+ getStyleComputedProperty(parent, 'overflow')
+ ) !== -1 ||
+ ['scroll', 'auto'].indexOf(
+ getStyleComputedProperty(parent, 'overflow-x')
+ ) !== -1 ||
+ ['scroll', 'auto'].indexOf(
+ getStyleComputedProperty(parent, 'overflow-y')
+ ) !== -1
+ ) {
+ // 如果检测到的 scrollParent 是 body,我们将对其父元素做一次额外的检测
+ // 这样在 Chrome 系的浏览器中会得到 body,其他情况下会得到 documentElement
+ // 修复 issue #65
+ return parent
+ }
+ return getScrollParent(element.parentNode)
+ }
+
+ /**
+ * 判断给定元素是否固定或者在一个固定元素中
+ * @function
+ * @ignore
+ * @argument {Element} element 给定的元素
+ * @argument {Element} customContainer 自定义的容器
+ * @returns {Boolean}
+ */
+ isFixed = element => {
+ const { getStyleComputedProperty, isFixed, getNodeName } = this
+
+ if (['html', 'body', '#document'].includes(getNodeName(element))) {
+ // $FlowFixMe: assume body is always available
+ return false
+ }
+ if (getStyleComputedProperty(element, 'position') === 'fixed') {
+ // position 为 fixed
+ return true
+ }
+ // 判断父元素是否固定
+ return element.parentNode ? isFixed(element.parentNode) : element
+ }
+ /**
+ * 获取给定元素的 CSS 计算属性
+ * @function
+ * @ignore
+ * @argument {Eement} element 给定的元素
+ * @argument {String} property 属性
+ */
+ getStyleComputedProperty = (element, property) => {
+ // 注:这里会访问 DOM
+ const css = window.getComputedStyle(element, null)
+ return css[property]
+ }
+
+ _getPosition = (popper, reference) => {
+ const { getOffsetParent, isFixed } = this
+ var container = getOffsetParent(reference) // 获取父元素的偏移
+
+ if (this._options.forceAbsolute) {
+ // 强制使用绝对定位
+ return 'absolute'
+ }
+
+ // 判断 popper 是否使用固定定位
+ // 如果相关元素位于固定定位的元素中,popper 也应当使用固定固定定位来使它们可以同步滚动
+ var isParentFixed = isFixed(reference, container)
+ return isParentFixed ? 'fixed' : 'absolute'
+ }
+
+ getOffsetParent = element => {
+ // 注:这里会访问 DOM
+ var offsetParent = element.offsetParent
+ return offsetParent === root.document.body || !offsetParent
+ ? root.document.documentElement
+ : offsetParent
+ }
+ getOffsetRectRelativeToCustomParent = (element, parent, fixed) => {
+ const { getScrollParent, getBoundingClientRect } = this
+ var elementRect = getBoundingClientRect(element)
+ var parentRect = getBoundingClientRect(parent)
+
+ if (fixed) {
+ // 固定定位
+ var scrollParent = getScrollParent(parent)
+ parentRect.top += scrollParent.scrollTop
+ parentRect.bottom += scrollParent.scrollTop
+ parentRect.left += scrollParent.scrollLeft
+ parentRect.right += scrollParent.scrollLeft
+ }
+
+ var rect = {
+ top: elementRect.top - parentRect.top,
+ left: elementRect.left - parentRect.left,
+ bottom: elementRect.top - parentRect.top + elementRect.height,
+ right: elementRect.left - parentRect.left + elementRect.width,
+ width: elementRect.width,
+ height: elementRect.height
+ }
+ return rect
+ }
+ /**
+ * Get bounding client rect of given element
+ * 获取给定元素的边界
+ * @function
+ * @ignore
+ * @param {HTMLElement} element
+ * @return {Object} client rect
+ */
+ getBoundingClientRect = element => {
+ var rect = element.getBoundingClientRect()
+
+ // IE11以下
+ var isIE = navigator.userAgent.indexOf('MSIE') !== -1
+
+ // 修复 IE 的文档的边界 top 值总是 0 的bug
+ var rectTop =
+ isIE && element.tagName === 'HTML' ? -element.scrollTop : rect.top
+
+ return {
+ left: rect.left,
+ top: rectTop,
+ right: rect.right,
+ bottom: rect.bottom,
+ width: rect.right - rect.left,
+ height: rect.bottom - rectTop
+ }
+ }
+
+ /**
+ * 移除更新 popper 位置时用到的事件监听器
+ * @method
+ * @memberof Popper
+ * @access private
+ */
+ removeEventListeners = element => {
+ const { getScrollParent } = this
+
+ // 注:这里会访问 DOM
+ // 移除 resize 事件监听
+ root.removeEventListener('resize', this.scrollCallback)
+ if (boundariesElement !== 'window') {
+ // 如果边界元素不是窗口,说明还监听了滚动事件
+ var target = getScrollParent(element)
+ if (
+ target === root.document.body ||
+ target === root.document.documentElement
+ ) {
+ target = root
+ }
+ // 移除滚动事件监听
+ target.removeEventListener('scroll', this.scrollCallback)
+ }
+ // 更新回调摄者为空
+ this.scrollCallback = null
+ }
+ /**
+ * 防止overlay的合适的位置位置 在auto时候有效
+ * @method
+ * @memberof Popper
+ * @ignore
+ * @argument {Eement} element 给定的元素
+ * @argument {String} property 属性
+ */
+}
diff --git a/components/popper/utils/positionUtils.js b/components/popper/utils/positionUtils.js
new file mode 100644
index 000000000..551cb8bbc
--- /dev/null
+++ b/components/popper/utils/positionUtils.js
@@ -0,0 +1,207 @@
+import PopperJS from './popper'
+
+const { isBody, isFixed, getOffsetRectRelativeToCustomParent } = new PopperJS()
+// 上下防止溢出 就会定位到划出的边缘位置
+const overflowOffset = (placement, scrollTop, rect, top, left, width, props) => {
+ const { topGap, container } = props
+ let _top = top
+ switch (placement) {
+ case 'bottom-start':
+ _top = rect.top + rect.height + topGap <= 0 ? scrollTop + topGap : top
+ break
+ case 'top-start':
+ if (container.clientHeight + rect.height - rect.bottom <= 0) {
+ _top = top - (rect.top - container.clientHeight)
+ }
+ break
+ }
+ return {
+ top: _top,
+ width,
+ left,
+ placement
+ }
+}
+// 对于auto的计算方式
+const positionAuto = (attachEleRect, popperHeight, popperRef, height, containerHeight) => {
+ // auto时候 定位比较合适的位置
+ let placement = 'bottom-start'
+ popperHeight === undefined && (popperHeight = 0)
+ const width = popperRef ? popperRef.clientWidth : 0
+
+ if (popperRef || height) {
+ height && (popperHeight = height)
+ } else if (popperRef && popperRef.clientHeight && popperHeight !== popperRef.clientHeight) {
+ popperHeight = popperRef.clientHeight
+ }
+ // 上下都放不下的时候 放左右
+ if (attachEleRect.top + popperHeight + attachEleRect.height > containerHeight) {
+ if (attachEleRect.right > width) {
+ placement = 'right'
+ }
+ if (attachEleRect.left > width) {
+ placement = 'left'
+ }
+ } else if (attachEleRect.top < popperHeight) {
+ if (attachEleRect.right > width) {
+ placement = 'right'
+ }
+ if (attachEleRect.left > width) {
+ placement = 'left'
+ }
+ }
+ // 上下能放下的时候以上下优先放置
+
+ if (attachEleRect.top > popperHeight) {
+ placement = 'top-start'
+ }
+ if (attachEleRect.top + popperHeight + attachEleRect.height < containerHeight) {
+ placement = 'bottom-start'
+ }
+ return placement
+}
+const getPlacement = (attachEleRect, container, props, state) => {
+ let { popperHeight, popperRef } = state
+ let { attachEle, placement, height, width = 0, leftGap = 0 } = props
+
+ if (!attachEle) return
+ let containerHeight = document.documentElement.clientHeight || document.body.clientHeight
+
+ if (isFixed(attachEle)) {
+ containerHeight = container.clientHeight
+ }
+ if (isBody(container)) {
+ containerHeight = document.documentElement.clientHeight || document.body.clientHeight
+ }
+
+ let poperTop = attachEleRect.top + attachEleRect.height
+ const caclBottomOrTopPlacement = (bottomPlacement, topPlacement) => {
+ // 计算popper在元素上面或下面
+ placement = bottomPlacement
+ popperHeight === undefined && (popperHeight = 0) // 自动探测边界,第一次时需设置为不可见,否则会闪跳,用来设置class hi-popper__content--hide
+ if (popperRef || height) {
+ // 元素已挂载到dom且当前popper处于显示状态
+ if (height) {
+ popperHeight = height
+ } else if (popperRef.clientHeight && popperHeight !== popperRef.clientHeight) {
+ popperHeight = popperRef.clientHeight
+ }
+ poperTop += popperHeight
+ if (poperTop >= containerHeight) {
+ placement = topPlacement
+ }
+ }
+ }
+ const caclLeftOrRightPlacement = (leftPlacement, RightPlacement) => {
+ // 计算popper在元素上面或下面
+ placement = leftPlacement
+ const _width = popperRef ? popperRef.clientWidth : width
+ if (attachEleRect.right > _width + leftGap) {
+ placement = RightPlacement
+ }
+ if (attachEleRect.left > _width + leftGap) {
+ placement = leftPlacement
+ }
+ }
+ if (placement === 'top-bottom-start') {
+ caclBottomOrTopPlacement('bottom-start', 'top-start')
+ } else if (placement === 'top-bottom') {
+ caclBottomOrTopPlacement('bottom', 'top')
+ } else if (placement === 'left-right-start') {
+ caclLeftOrRightPlacement('left-start', 'right-start')
+ } else if (placement === 'left-right') {
+ caclLeftOrRightPlacement('left', 'right')
+ } else if (placement === 'auto') {
+ positionAuto(attachEleRect, popperHeight, popperRef, height, containerHeight)
+ }
+ return placement || 'bottom-start'
+}
+export const getOffset = (props, state, status) => {
+ let { attachEle, topGap, leftGap, width, container, preventOverflow } = props
+ if (!attachEle) return
+
+ const { popperHeight, popperWidth } = state
+ let rect = attachEle.getBoundingClientRect()
+
+ if (isFixed(attachEle) && !isBody(container)) {
+ rect = getOffsetRectRelativeToCustomParent(attachEle, container, isFixed(attachEle))
+ }
+
+ let _scrollTop = container.scrollTop
+ let _scrollLeft = container.scrollLeft
+ // 兼容处理
+ if (isBody(container)) {
+ _scrollTop = document.documentElement.scrollTop || document.body.scrollTop
+ _scrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft
+ }
+
+ let top = rect.top + _scrollTop
+ let left = rect.left + _scrollLeft
+
+ width = width === false ? popperWidth : width === undefined ? rect.width : width
+
+ let placement = getPlacement(rect, container, props, state) || 'bottom-start'
+ const rectHeight = rect.height
+ switch (placement) {
+ case 'bottom':
+ top = top + topGap + rectHeight
+ left = left + leftGap + rect.width / 2
+ break
+ case 'bottom-start':
+ top = top + topGap + rectHeight
+ left = left + leftGap
+ break
+ case 'bottom-end':
+ top = top + topGap + rectHeight
+ left = left + leftGap - width + rect.width
+ break
+ case 'top':
+ top = top - topGap
+ left = left + leftGap + rect.width / 2
+ break
+ case 'top-start':
+ top = top - topGap
+ left = left + leftGap
+ break
+ case 'top-end':
+ top = top - topGap
+ left = left + leftGap - width + rect.width
+ break
+ case 'left':
+ top = top + rect.height / 2
+ left = left - leftGap
+ break
+ case 'left-start':
+ top = top + topGap
+ left = left - rect.width
+ break
+ case 'left-end':
+ top = top + rect.height - topGap - popperHeight
+ left = left - rect.width
+ break
+
+ case 'right':
+ top = top + rect.height / 2
+ left = left + rect.width + leftGap
+ break
+ case 'right-start':
+ top = top + topGap
+ left = left + rect.width + leftGap
+ break
+ case 'right-end':
+ top = top + rect.height - topGap - popperHeight
+ left = left + rect.width + leftGap
+ break
+ }
+
+ if (preventOverflow) {
+ return overflowOffset((placement = 'bottom-start'), _scrollTop, rect, top, left, width, props)
+ }
+
+ return {
+ width,
+ top,
+ left,
+ placement: placement
+ }
+}
diff --git a/components/popper/utils/useClickOutside.js b/components/popper/utils/useClickOutside.js
new file mode 100644
index 000000000..dcffbed28
--- /dev/null
+++ b/components/popper/utils/useClickOutside.js
@@ -0,0 +1,28 @@
+import { useEffect, useCallback, useRef } from 'react'
+
+const useClickOutside = (onClickOutside, dom, eventName = 'click', attachEle) => {
+ const element = useRef('')
+ const handleOutside = useCallback(
+ (e) => {
+ const targetElement = typeof dom === 'function' ? dom() : dom
+ const el = targetElement || element.current
+ if (el) {
+ if (attachEle) {
+ !(attachEle.contains(e.target) || el.contains(e.target)) && onClickOutside(e)
+ } else {
+ !el.contains(e.target) && onClickOutside(e)
+ }
+ }
+ },
+ [onClickOutside, dom, element]
+ )
+ useEffect(() => {
+ // 使用事件捕获
+ document.addEventListener(eventName, handleOutside, true)
+ return () => {
+ document.removeEventListener(eventName, handleOutside, true)
+ }
+ }, [eventName, onClickOutside, element])
+ return element
+}
+export default useClickOutside
diff --git a/components/popper/utils/useWaitForDOMRef.js b/components/popper/utils/useWaitForDOMRef.js
new file mode 100644
index 000000000..0bdecc6a1
--- /dev/null
+++ b/components/popper/utils/useWaitForDOMRef.js
@@ -0,0 +1,39 @@
+import { useState, useEffect } from 'react'
+
+const ownerDocument = node => {
+ return (node && node.ownerDocument) || document
+}
+export const resolveContainerRef = ref => {
+ if (typeof document === 'undefined') return null
+ if (ref == null) return ownerDocument().body
+ if (typeof ref === 'function') ref = ref()
+
+ if (ref && 'current' in ref) ref = ref.current
+ if (ref && ref.nodeType) return ref || null
+
+ return null
+}
+
+export default function useWaitForDOMRef (ref, onResolved) {
+ const [resolvedRef, setRef] = useState(() => resolveContainerRef(ref))
+
+ if (!resolvedRef) {
+ const earlyRef = resolveContainerRef(ref)
+ if (earlyRef) setRef(earlyRef)
+ }
+
+ useEffect(() => {
+ if (onResolved && resolvedRef) {
+ onResolved(resolvedRef)
+ }
+ }, [onResolved, resolvedRef])
+
+ useEffect(() => {
+ const nextRef = resolveContainerRef(ref)
+ if (nextRef !== resolvedRef) {
+ setRef(nextRef)
+ }
+ }, [ref, resolvedRef])
+
+ return resolvedRef
+}
diff --git a/components/progress/BarProgress.js b/components/progress/BarProgress.js
index 20b9f8100..57aa261cc 100644
--- a/components/progress/BarProgress.js
+++ b/components/progress/BarProgress.js
@@ -1,62 +1,47 @@
import React from 'react'
import Classnames from 'classnames'
-export default class BarProgress extends React.Component {
- textRef = React.createRef()
- state = { insidePlacement: 'right' }
-
- componentDidMount () {
- if (this.props.placement === 'inside') {
- if (
- this.textRef.current &&
- this.textRef.current.clientWidth >= this.getWidth() - ((this.getWidth() * this.props.percent) / 100 + 5)
- ) {
- this.setState({ insidePlacement: 'left' })
- } else {
- this.setState({ insidePlacement: 'right' })
- }
- }
- }
- getWidth = () => {
- const { width, size } = this.props
- if (!width || width <= 0) {
- return size === 'large' ? 480 : 160
+export const BarProgress = (props) => {
+ function getWidth() {
+ if (!props.width || props.width <= 0) {
+ return props.size === 'large' ? 480 : 160
}
- return width
+ return props.width
}
- getHeight = () => {
- const { size, height } = this.props
+
+ function getHeight() {
+ const { size, height } = props
if (!height || height <= 0) {
return size === 'large' ? 8 : size === 'default' ? 6 : 2
}
return height
}
- render () {
- let prefix = 'hi-progress'
- const { percent: percentNum, content, type, showInfo, placement, tooltip = null, active } = this.props
- const percent = percentNum > 0 ? percentNum : 0
- return (
-
-
-
- {showInfo && placement === 'inside' && this.getHeight() >= 14 && (
-
- {content || `${percent}%`}
-
- )}
- {tooltip}
-
+ const prefix = 'hi-progress'
+ const { percent: percentNum, placement, tooltip = null, active } = props
+ const content = typeof props.content !== 'undefined' ? props.content : props.text // // api 兼容 1.x 为 text 2.x 改为 content
+ const showInfo = typeof props.showInfo !== 'undefined' ? props.showInfo : props.withOutText // // api 兼容 1.x 为 withOutText 2.x 改为 showInfo
+ const type = props.type || props.status
+
+ const percent = percentNum > 0 ? percentNum : 0
+ return (
+
+
+
+ {showInfo && placement === 'inside' && getHeight() >= 14 && (
+
{content || `${percent}%`}
+ )}
+ {tooltip}
- {showInfo && placement === 'outside' && (
-
{content || `${percent}%`}
- )}
- )
- }
+ {showInfo && placement === 'outside' && (
+
{content || `${percent}%`}
+ )}
+
+ )
}
diff --git a/components/progress/Progress.js b/components/progress/Progress.js
index 9615456a8..c4a933f15 100644
--- a/components/progress/Progress.js
+++ b/components/progress/Progress.js
@@ -1,7 +1,7 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import './style'
-import BarProgress from './BarProgress.js'
+import { BarProgress } from './BarProgress.js'
import { CircleProgress } from './CircleProgress.js'
import { DashboardProgress } from './DashboardProgress'
import Provider from '../context'
@@ -40,9 +40,13 @@ class Progress extends Component {
render () {
let prefix = 'hi-progress'
- const { className = '', apperance, theme } = this.props
-
- return
{this.getRenderType(apperance)}
+ const { className = '', theme } = this.props
+ const apperance = this.props.apperance || this.props.type // api 兼容 1.x 为 type 2.x 改为 apperance
+ return (
+
+ {this.getRenderType(apperance)}
+
+ )
}
}
export default Provider(Progress)
diff --git a/components/progress/index.d.ts b/components/progress/index.d.ts
new file mode 100644
index 000000000..cd03301da
--- /dev/null
+++ b/components/progress/index.d.ts
@@ -0,0 +1,14 @@
+interface Props {
+ apperance?: 'bar' | 'circle' | 'dashboard'
+ size?: 'large' | 'default' | 'small'
+ active?: boolean
+ content?: string | JSX.Element
+ showInfo?: boolean
+ type?: 'primary' | 'success' | 'warning' | 'error'
+ radius?: number
+ placement?: 'inside' | 'outside'
+ width?: number
+ height?: number
+}
+declare const Progress: React.ComponentType
+export default Progress
diff --git a/components/progress/index.js b/components/progress/index.js
index c91ed2711..5cc755de0 100644
--- a/components/progress/index.js
+++ b/components/progress/index.js
@@ -1,11 +1,4 @@
import Progress from './Progress'
-import { depreactedPropsCompat } from '../_util'
import './style/index'
-export default depreactedPropsCompat([
- ['apperance', 'type'],
- ['content', 'text'],
- ['showInfo', 'withOutText'],
- ['type', 'status'],
- ['placement', 'inside', (data) => (data ? 'inside' : 'outside')]
-])(Progress)
+export default Progress
diff --git a/components/progress/style/index.scss b/components/progress/style/index.scss
index ab7d4f898..b664f4caa 100644
--- a/components/progress/style/index.scss
+++ b/components/progress/style/index.scss
@@ -1,15 +1,14 @@
-@import '@hi-ui/core-css/index.scss';
+@import '../../core-css/index.scss';
-$progress-bg: #f2f2f2 !default;
-$progress-primary: #3b76dc !default;
-$progress-success: #1da653 !default;
-$progress-warn: #e19d0c !default;
-$progress-error: #eb5252 !default;
-$progress-text: #666 !default;
+$color-map: (
+ 'warn': get-color($palette-secondary, 'warning'),
+ 'success': get-color($palette-secondary, 'success'),
+ 'error': get-color($palette-secondary, 'danger')
+) !default;
.hi-progress {
&__inner {
- background-color: $progress-bg;
+ background-color: use-color('gray-10');
border-radius: 10px;
position: relative;
overflow: hidden;
@@ -24,7 +23,7 @@ $progress-text: #666 !default;
right: 0;
bottom: 0;
left: 0;
- background: #fff;
+ background: use-color('white');
opacity: 0;
content: '';
animation: hi-progress-active 2.4s cubic-bezier(0.23, 1, 0.32, 1) infinite;
@@ -35,55 +34,46 @@ $progress-text: #666 !default;
height: 100%;
display: block;
position: relative;
- background-color: $progress-primary;
+ background-color: use-color('primary');
transition: width 0.4s cubic-bezier(0.3, 0, 0.7, 1);
- }
-
- &__bar--success {
- background-color: $progress-success;
- }
- &__bar--warn {
- background-color: $progress-warn;
- }
+ &--primary {
+ background-color: use-color('primary');
+ }
- &__bar--error {
- background-color: $progress-error;
+ @each $key, $value in $color-map {
+ &--#{$key} {
+ background-color: $value;
+ }
+ }
}
&__text {
padding-left: 25px;
font-size: 14px;
- color: $progress-text;
+ color: use-color('gray-80');
display: inline-block;
vertical-align: middle;
+
+ &--primary {
+ color: use-color('primary');
+ }
+
+ @each $key, $value in $color-map {
+ &--#{$key} {
+ color: $value;
+ }
+ }
}
&__text--inside {
font-size: 14px;
+ color: use-color('white');
position: absolute;
- right: -5px;
+ right: 5px;
top: 50%;
- transform: translate(100%, -50%);
+ transform: translateY(-50%);
line-height: 14px;
-
- &.inside--left {
- color: #fff;
- right: 5px;
- transform: translate(0, -50%);
- }
- }
-
- &__text--success {
- color: $progress-success;
- }
-
- &__text--warn {
- color: $progress-warn;
- }
-
- &__text--error {
- color: $progress-error;
}
&__svg {
@@ -113,85 +103,29 @@ $progress-text: #666 !default;
&__circle {
fill: none;
- stroke: $progress-primary;
+ stroke: use-color('primary');
stroke-width: 6;
transform: rotate(-90deg);
transform-origin: 50% 50%;
transition: stroke-dashoffset 0.4s cubic-bezier(0.3, 0, 0.7, 1);
- }
-
- &__circle--success {
- stroke: $progress-success;
- }
- &__circle--warn {
- stroke: $progress-warn;
- }
-
- &__circle--error {
- stroke: $progress-error;
+ @each $key, $value in $color-map {
+ &--#{$key} {
+ stroke: $value;
+ }
+ }
}
&__dashboard {
fill: none;
- stroke: $progress-primary;
+ stroke: use-color('primary');
stroke-width: 6;
transition: stroke-dasharray 0.4s cubic-bezier(0.3, 0, 0.7, 1);
- }
-
- &__dashboard--success {
- stroke: $progress-success;
- }
-
- &__dashboard--warn {
- stroke: $progress-warn;
- }
-
- &__dashboard--error {
- stroke: $progress-error;
- }
-}
-
-@each $key, $value in $theme-colors {
- .theme__#{$key} {
- .hi-progress__bar--primary {
- background-color: $value;
- }
-
- .hi-progress__text--primary {
- color: $value;
- }
- .hi-progress__circle {
- stroke: $value;
-
- &--success {
- stroke: $progress-success;
- }
-
- &--warn {
- stroke: $progress-warn;
+ @each $key, $value in $color-map {
+ &--#{$key} {
+ stroke: $value;
}
-
- &--error {
- stroke: $progress-error;
- }
- }
-
- .hi-progress__dashboard {
- stroke: $value;
- }
-
- .hi-progress__dashboard--success {
- stroke: $progress-success;
- }
-
- .hi-progress__dashboard--warn {
- stroke: $progress-warn;
- }
-
- .hi-progress__dashboard--error {
- stroke: $progress-error;
}
}
}
diff --git a/components/radio/index.d.ts b/components/radio/index.d.ts
new file mode 100644
index 000000000..02a966638
--- /dev/null
+++ b/components/radio/index.d.ts
@@ -0,0 +1,28 @@
+type DataItem = {
+ content: string
+ id: string | number
+ disabled?: boolean
+}
+interface Props {
+ value?: string | number
+ autoFocus?: boolean
+ checked?: boolean
+ disabled?: boolean
+ onChange?: (event: ChangeEvent) => void
+}
+interface GroupProps {
+ placement?: 'vertical' | 'horizontal'
+ data: DataItem[]
+ value?: string | number
+ defaultValue?: string | number
+ disabled?: boolean
+ type?: 'default' | 'button'
+ onChange?: (value: string) => void
+}
+declare class Group extends React.Component {
+}
+declare class Radio extends React.Component {
+ static Group = Group
+}
+export default Radio
+
diff --git a/components/radio/index.js b/components/radio/index.js
index db757ab1e..e12a45663 100644
--- a/components/radio/index.js
+++ b/components/radio/index.js
@@ -1,12 +1,14 @@
+import React from 'react'
import Radio from './Radio'
import RadioLegacy from './radio-legacy/index'
import Group from './Group'
-import SwitchVersion from '../_util/SwitchVersion'
import './style/index'
-const VRadio = SwitchVersion(Radio, RadioLegacy)
-const VGroup = SwitchVersion(Group, undefined)
+const VRadio = ({ legacy, ...props }) => {
+ const WrapperComponent = legacy ? RadioLegacy : Radio
+ return
+}
-VRadio.Group = VGroup
+VRadio.Group = Group
export default VRadio
diff --git a/components/radio/style/index.scss b/components/radio/style/index.scss
index c50162a9e..fd8ea5b1f 100644
--- a/components/radio/style/index.scss
+++ b/components/radio/style/index.scss
@@ -1,8 +1,8 @@
-@import '@hi-ui/core-css/index.scss';
+@import '../../core-css/index.scss';
$prefixCls: '.hi-radio' !default;
-@mixin hi-radio-style($active-color: get-color($palette-primary, 'hiui-blue')) {
+@mixin hi-radio-style($active-color: use-color('primary')) {
&:not(#{$prefixCls}--disabled):hover {
#{$prefixCls}__input {
border-color: $active-color;
@@ -53,8 +53,6 @@ $prefixCls: '.hi-radio' !default;
height: 32px;
vertical-align: middle;
- @include component-reset();
-
input {
cursor: pointer;
position: absolute;
@@ -93,7 +91,7 @@ $prefixCls: '.hi-radio' !default;
vertical-align: middle;
width: 16px;
height: 16px;
- border: 1px solid #d8d8d8;
+ border: 1px solid use-color('gray-30');
transition: 0.3s ease-out;
line-height: 1;
border-radius: 50%;
@@ -127,16 +125,16 @@ $prefixCls: '.hi-radio' !default;
cursor: not-allowed;
#{$prefixCls}__input {
- border-color: #d8d8d8;
- background: #f2f2f2;
+ border-color: use-color('gray-30');
+ background: use-color('gray-10');
&::after {
- background: #d8d8d8;
+ background: use-color('gray-30');
}
}
#{$prefixCls}__text {
- color: rgba(#000, 0.65);
+ color: use-color('gray-80');
}
}
@@ -172,39 +170,3 @@ $prefixCls: '.hi-radio' !default;
@include hi-radio-style();
}
-
-@each $key, $value in $theme-colors {
- .theme__#{$key$prefixCls} {
- @include hi-radio-style($value);
- }
-
- .theme__#{$key}#{$prefixCls}:not(#{$prefixCls}--disabled):hover {
- #{$prefixCls}__input {
- border-color: $value;
- }
- }
-
- .theme__#{$key}#{$prefixCls} {
- input {
- &:focus + #{$prefixCls}__input {
- border-color: $value;
- }
- }
-
- &__button.hi-btn {
- {$prefixCls}__button--checked {
- border-color: $value;
- color: $value;
- background-color: rgba($value, 0.1);
-
- #{$prefixCls} {
- color: $value;
- }
- }
-
- &:not(.hi-btn--disabled):hover {
- border-color: $value;
- }
- }
- }
-}
diff --git a/components/rate/Rate.js b/components/rate/Rate.js
index b96e86239..8e013a3f8 100644
--- a/components/rate/Rate.js
+++ b/components/rate/Rate.js
@@ -1,133 +1,147 @@
-import React, { Component } from 'react'
-import PropTypes from 'prop-types'
+import React, { useState, useEffect } from 'react'
import classnames from 'classnames'
import * as Icons from './Icons'
import ToolTip from '../tooltip'
-class Rate extends Component {
- constructor (props) {
- super(props)
- const { value, defaultValue } = props
- this.state = {
- value: value === undefined ? defaultValue : value
+const Rate = (props) => {
+ const {
+ value: trueVal,
+ disabled,
+ useEmoji,
+ allowHalf,
+ character,
+ renderCharacter,
+ defaultValue,
+ className,
+ style,
+ count,
+ prefixCls,
+ tooltips,
+ color,
+ vertical,
+ onChange,
+ descRender,
+ readOnly
+ } = props
+ const clearable = props.clearable || props.allowClear // 兼容API 2.x 改为clearable
+ const [value, setValue] = useState(trueVal === undefined ? defaultValue : trueVal)
+
+ const [hoverValue, setHoverValue] = useState(0)
+
+ useEffect(() => {
+ if (trueVal !== undefined) {
+ setValue(trueVal)
}
- }
- static getDerivedStateFromProps ({ value }) {
- if (value !== undefined) {
- return {
- value
- }
+ }, [trueVal])
+
+ const handleIconLeave = () => {
+ if (disabled) {
+ return
}
- return null
- }
- state = {
- value: 0,
- hoverValue: 0
+ setHoverValue(0)
}
- renderIcon = (idx) => {
- const { useEmoji, allowHalf, disabled } = this.props
- const { value, hoverValue } = this.state
- let currentValue = hoverValue || value
- if (!allowHalf || useEmoji) {
- currentValue = Math.ceil(currentValue)
+
+ const handleIconEnter = (hoverValue) => {
+ if (disabled || readOnly) {
+ return
}
- return (
-
- )
+
+ setHoverValue(hoverValue)
}
- handleIconClick = (value) => {
- const { allowHalf, clearable, onChange, disabled } = this.props
- if (disabled) {
+
+ const handleIconClick = (valueIndex) => {
+ if (disabled || readOnly) {
return
}
if (!allowHalf) {
- value = Math.ceil(value)
+ valueIndex = Math.ceil(valueIndex)
}
- if (value === this.state.value && clearable) {
- onChange && onChange({ value: 0 })
- this.setState({
- value: 0
- })
+ if (valueIndex === value && clearable) {
+ onChange && onChange(0)
+ if (trueVal === undefined) {
+ setValue(0)
+ }
return
}
- onChange && onChange(value)
- this.setState({ value })
- }
- handleIconEnter = (hoverValue) => {
- if (this.props.disabled) {
- return
+
+ if (trueVal === undefined) {
+ setValue(valueIndex)
}
- this.setState({ hoverValue })
+ onChange && onChange(valueIndex)
}
- handleIconLeave = () => {
- if (this.props.disabled) {
- return
+
+ const renderIcon = (idx) => {
+ let currentValue = hoverValue || value
+ if (!allowHalf) {
+ currentValue = Math.ceil(currentValue)
}
- this.setState({ hoverValue: 0 })
- }
- render () {
- const {
- className,
- style,
- count,
- useEmoji,
- prefixCls,
- tooltips,
- disabled
- } = this.props
- const iconCount = Math.ceil(useEmoji ? 5 : count)
- const iconHalfCls = `${prefixCls}__star__half`
- const starCls = classnames(`${prefixCls}__star`, {
- [`${prefixCls}__star--disabled`]: disabled
- })
return (
-
+
+ )
+ }
+
+ const currentValue = hoverValue || value
+ const iconCount = Math.ceil(useEmoji ? 5 : count)
+ const iconHalfCls = `${prefixCls}__star__half`
+ const starCls = classnames(`${prefixCls}__star`, {
+ [`${prefixCls}__star--disabled`]: disabled,
+ [`${prefixCls}__star--readOnly`]: readOnly
+ })
+
+ const descCls = classnames(`${prefixCls}__desc`)
+
+ return (
+
+
{Array(iconCount)
.fill()
.map((_, idx) => {
- const value = idx + 1
- const halfValue = idx + 0.5
+ const indexValue = idx + 1
+ const halfValue = allowHalf ? idx + 0.5 : indexValue
return (
-
this.handleIconEnter(halfValue)}
- onMouseMove={() => this.handleIconEnter(halfValue)}
- onClick={() => this.handleIconClick(halfValue)}
- />
+ className={classnames(iconHalfCls, `${iconHalfCls}--${vertical ? 'top' : 'left'}`, {
+ grayscale: vertical ? indexValue > currentValue : currentValue < halfValue
+ })}
+ onMouseEnter={() => handleIconEnter(vertical ? indexValue : halfValue)}
+ onMouseMove={() => handleIconEnter(vertical ? indexValue : halfValue)}
+ onClick={() => handleIconClick(vertical ? indexValue : halfValue)}
+ >
+ {renderIcon(indexValue)}
+
+
this.handleIconEnter(value)}
- onMouseMove={() => this.handleIconEnter(value)}
- onClick={() => this.handleIconClick(value)}
- />
- {this.renderIcon(value)}
+ className={classnames(iconHalfCls, `${iconHalfCls}--${vertical ? 'bottom' : 'right'}`, {
+ grayscale: vertical ? currentValue < halfValue : indexValue > currentValue
+ })}
+ onMouseEnter={() => handleIconEnter(vertical ? halfValue : indexValue)}
+ onMouseMove={() => handleIconEnter(vertical ? halfValue : indexValue)}
+ onClick={() => handleIconClick(vertical ? halfValue : indexValue)}
+ >
+ {renderIcon(indexValue)}
+
)
})}
- )
- }
-}
-
-Rate.propTypes = {
- useEmoji: PropTypes.bool,
- clearable: PropTypes.bool,
- allowHalf: PropTypes.bool,
- className: PropTypes.string,
- defaultValue: PropTypes.number,
- disabled: PropTypes.bool,
- style: PropTypes.object,
- tooltips: PropTypes.arrayOf(PropTypes.string),
- value: PropTypes.number,
- count: PropTypes.number,
- onChange: PropTypes.func
+ {descRender &&
{descRender(hoverValue || value)}}
+
+ )
}
Rate.defaultProps = {
@@ -136,36 +150,42 @@ Rate.defaultProps = {
count: 5,
prefixCls: 'hi-rate',
tooltips: [],
- onChange: () => { }
+ desc: [],
+ onChange: () => {},
+ style: {
+ fontSize: 24
+ },
+ color: '#FFCA28',
+ vertical: false
}
-function ToolTipWrapper ({ children, title }) {
+function ToolTipWrapper({ children, title }) {
return title ? {children} : children
}
-function Icon ({ value, currentValue, disabled, useEmoji, allowHalf }) {
+function Icon({ value, currentValue, disabled, useEmoji, allowHalf, character, renderCharacter, readOnly }) {
+ if (renderCharacter) {
+ return renderCharacter(currentValue, value)
+ }
+ if (character) {
+ return character
+ }
if (useEmoji) {
const emojiValue = currentValue > 5 ? 5 : currentValue
- const Emojis = [
- Icons.EmojiOne,
- Icons.EmojiTwo,
- Icons.EmojiThree,
- Icons.EmojiFour,
- Icons.EmojiFive
- ]
+ const Emojis = [Icons.EmojiOne, Icons.EmojiTwo, Icons.EmojiThree, Icons.EmojiFour, Icons.EmojiFive]
if (value <= emojiValue) {
return React.createElement(Emojis[emojiValue - 1])
} else {
return
}
}
+
if (value <= currentValue) {
return
} else if (value === currentValue + 0.5 && allowHalf) {
- return disabled ? :
+ return disabled || readOnly ? :
} else {
- return disabled ? :
+ return disabled || readOnly ? :
}
}
-
export default Rate
diff --git a/components/rate/index.d.ts b/components/rate/index.d.ts
new file mode 100644
index 000000000..c4465fdeb
--- /dev/null
+++ b/components/rate/index.d.ts
@@ -0,0 +1,18 @@
+interface Props {
+ clearable?: boolean
+ allowHalf?: boolean
+ useEmoji?: boolean
+ disabled?: boolean
+ count?: number
+ readOnly?: boolean
+ defaultValue?: number
+ value?: number
+ tooltips?: string[]
+ descRender?: (value: number, index: number) => JSX.Element
+ character?: string | JSX.Element
+ color?: string
+ characterRender?: (value: number, index: number) => JSX.Element
+ onChange?: (value: number) => void
+}
+declare const Rate: React.ComponentType
+export default Rate
diff --git a/components/rate/index.js b/components/rate/index.js
index af3fdbcdf..a837784b6 100644
--- a/components/rate/index.js
+++ b/components/rate/index.js
@@ -1,5 +1,4 @@
import './style/index'
import Rate from './Rate'
-import { depreactedPropsCompat } from '../_util'
-export default depreactedPropsCompat([['clearable', 'allowClear']])(Rate)
+export default Rate
diff --git a/components/rate/style/index.scss b/components/rate/style/index.scss
index cd1bada15..b66a461b7 100644
--- a/components/rate/style/index.scss
+++ b/components/rate/style/index.scss
@@ -14,8 +14,6 @@
display: inline-block;
transition: 0.3s ease;
margin: 0 6px !important;
- width: $w;
- height: $h;
&:active,
&:hover {
@@ -30,35 +28,76 @@
}
}
+ &--readOnly {
+ cursor: auto;
+
+ &:hover {
+ transform: none;
+ }
+ }
+
&__half {
- height: 100%;
width: 50%;
display: inline-block;
- position: absolute;
z-index: 1;
+ &.grayscale {
+ filter: grayscale(100%);
+ }
+
&--wrapper {
position: absolute;
top: 0;
left: 0;
- height: $h;
- width: $w;
+ }
+
+ &--left {
+ width: 50%;
+ position: absolute;
+ z-index: 10;
+ overflow: hidden;
}
&--right {
right: 0;
+ width: 100%;
+ }
+
+ &--top {
+ height: 50%;
+ position: absolute;
+ z-index: 10;
+ width: 100%;
+ overflow: hidden;
+ }
+
+ &--bottom {
+ top: 0;
+ width: 100%;
+ height: 100%;
}
}
}
+ &__desc {
+ margin-left: 14px;
+ display: flex;
+ padding-top: 4px;
+ }
+
&__icon {
display: inline-block;
- width: 100%;
- height: 100%;
+ width: $w;
+ height: $h;
svg {
- width: 100%;
- height: 100%;
+ width: $w;
+ height: $h;
}
}
+
+ &__outter {
+ display: flex;
+ flex-direction: row;
+ }
}
diff --git a/components/rich-text-editor/index.js b/components/rich-text-editor/index.js
new file mode 100644
index 000000000..44caca3dd
--- /dev/null
+++ b/components/rich-text-editor/index.js
@@ -0,0 +1,46 @@
+import React, { forwardRef } from 'react'
+import classNames from 'classnames'
+
+import ReactQuill from 'react-quill'
+
+import Tooltip from '../tooltip'
+import './style/index'
+
+const QuillBarTooltip = (props) => {
+ const { toolbarsName, tooltipTitle, children, showTooltip = true } = props
+ return (
+
+ {showTooltip ? (
+
+ {children || }
+
+ ) : (
+
+ {children || }
+
+ )}
+
+ )
+}
+const InternalQuill = (props) => {
+ const { className, innerRef, theme } = props
+
+ return (
+ {
+ innerRef && (innerRef.current = el)
+ }}
+ className={classNames({
+ 'hi-quill-content': !theme,
+ className
+ })}
+ />
+ )
+}
+
+const RichTextEditor = forwardRef((props, ref) => {
+ return
+})
+export default RichTextEditor
+export { QuillBarTooltip }
diff --git a/components/rich-text-editor/style/index.js b/components/rich-text-editor/style/index.js
new file mode 100644
index 000000000..67206756d
--- /dev/null
+++ b/components/rich-text-editor/style/index.js
@@ -0,0 +1,4 @@
+import 'react-quill/dist/quill.snow.css'
+import 'react-quill/dist/quill.bubble.css'
+import 'react-quill/dist/quill.core.css'
+import './index.scss'
diff --git a/components/rich-text-editor/style/index.scss b/components/rich-text-editor/style/index.scss
new file mode 100644
index 000000000..1bff4c17d
--- /dev/null
+++ b/components/rich-text-editor/style/index.scss
@@ -0,0 +1,116 @@
+@import '../../core-css/index.scss';
+
+.hi-quill-content,
+.hi-quill-toolbar-tooltip {
+ .hi-quill-custom {
+ display: inline-block;
+ }
+
+ .ql-toolbar,
+ .hi-quill-toolbar {
+ button,
+ .ql-color-picker {
+ width: 32px;
+ height: 32px;
+ border-radius: 2px;
+ text-align: center;
+ transition: all 0.3s;
+ line-height: 32px;
+
+ &:hover {
+ background-color: use-color('primary-20');
+ }
+
+ svg {
+ float: none;
+ height: 16px;
+ }
+ }
+
+ .ql-picker {
+ height: 32px;
+ line-height: 30px;
+ border-radius: 2px;
+ border: 1px solid transparent;
+
+ &:focus {
+ outline: none;
+ }
+
+ &-label:focus {
+ border-color: transparent;
+ outline: none;
+ }
+
+ &.ql-expanded .ql-picker-label {
+ border-color: transparent;
+ }
+
+ &:hover {
+ border: 1px solid use-color('primary');
+ }
+
+ &-options {
+ padding: 4px 0;
+
+ .ql-picker-item {
+ &:hover {
+ background-color: use-color('primary-20');
+ }
+
+ &:focus {
+ outline: none;
+ }
+ }
+ }
+ }
+
+ .ql-font,
+ .ql-size,
+ .ql-header {
+ .ql-picker-options {
+ .ql-picker-item {
+ padding-left: 8px;
+ padding-right: 8px;
+ }
+ }
+ }
+
+ .ql-formats {
+ &::after {
+ content: '|';
+ float: right;
+ margin-left: 15px;
+ color: use-color('gray-50');
+ clear: none;
+ line-height: 32px;
+ }
+ }
+ }
+
+ .hi-upload {
+ display: inline-block;
+
+ button {
+ width: auto;
+ background-color: use-color('primary');
+ color: use-color('white');
+ padding: 4px 15px;
+ line-height: 22px;
+
+ &:hover {
+ background-color: use-color('primary-60');
+ color: use-color('white');
+ }
+ }
+ }
+}
+
+.hi-quill-toolbar-tooltip {
+ display: inline-block;
+ height: 32px;
+}
+
+.ql-container {
+ min-height: 120px;
+}
diff --git a/components/search/index.d.ts b/components/search/index.d.ts
new file mode 100644
index 000000000..4303b0823
--- /dev/null
+++ b/components/search/index.d.ts
@@ -0,0 +1,20 @@
+type Item = {
+ id: string | number
+ title: string | JSX.Element
+}
+interface DataItem extends Item{
+ children?: Item[]
+}
+interface Props {
+ prepend?: string | JSX.Element
+ append?: JSX.Element
+ disabled?: boolean
+ loading?: boolean
+ placeholder?: string
+ data?: DataItem
+ onSearch?: (inputVal: string, item?: DataItem) => void
+ onChange?: (e: React.ChangeEvent) => void
+ overlayClassName?: string
+}
+declare const Search: React.ComponentType
+export default Search
diff --git a/components/search/index.js b/components/search/index.js
new file mode 100644
index 000000000..85000662c
--- /dev/null
+++ b/components/search/index.js
@@ -0,0 +1,124 @@
+import React, { useState, useRef, useEffect, useCallback } from 'react'
+import Input from '../input'
+import Button from '../button'
+import SearchDropdown from './searchDropdown'
+import Provider from '../context'
+
+import './style'
+
+const prefixCls = 'hi-search'
+
+const Search = ({
+ onChange,
+ onSearch,
+ style,
+ placeholder,
+ prepend,
+ disabled,
+ data,
+ value,
+ loading,
+ localeDatas,
+ overlayClassName,
+ append,
+ theme
+}) => {
+ const [dropdownShow, setDropdownShow] = useState(false)
+ const searchInputContainer = useRef(null)
+ const [inputVal, setInputVal] = useState(value || '')
+
+ useEffect(() => {
+ setInputVal(value)
+ }, [value])
+
+ const closeDropdown = () => {
+ setDropdownShow(false)
+ }
+
+ const optionsClick = useCallback(
+ (value, item) => {
+ setInputVal(value)
+ setDropdownShow(false)
+ onSearch && onSearch(value, item)
+ },
+ [onSearch]
+ )
+
+ return (
+
+
+ {
+ const evt = window.event || e
+ if (evt.keyCode === 13) {
+ onSearch && onSearch(inputVal)
+ }
+ }}
+ onFocus={() => {
+ data && data.length > 0 && setDropdownShow(true)
+ }}
+ onChange={(e) => {
+ const { value } = e.target
+ setInputVal(value)
+ data && value.length > 0 && setDropdownShow(true)
+ onChange && onChange(value)
+ }}
+ />
+ {append ? (
+ {
+ e.preventDefault()
+ setDropdownShow(true)
+ onSearch && onSearch(inputVal)
+ }}
+ >
+ {append}
+
+ ) : (
+
+ {data && (
+
{
+ closeDropdown()
+ }}
+ data={data}
+ loading={loading}
+ dropdownShow={dropdownShow}
+ localeDatas={localeDatas}
+ theme={theme}
+ searchInputContainer={searchInputContainer}
+ />
+ )}
+
+ )
+}
+
+Search.defaultProps = {
+ style: { width: 200 },
+ loading: false
+}
+export default Provider(Search)
diff --git a/components/search/searchDropdown.js b/components/search/searchDropdown.js
new file mode 100644
index 000000000..8fe803490
--- /dev/null
+++ b/components/search/searchDropdown.js
@@ -0,0 +1,130 @@
+import React, { useRef, useEffect, useState, useCallback } from 'react'
+import classNames from 'classnames'
+import Popper from '../popper'
+import Loading from '../loading'
+
+const SearchDropdown = ({
+ data,
+ prefixCls,
+ optionsClick,
+ inputVal = '',
+ dropdownShow,
+ searchInputContainer,
+ localeDatas,
+ theme,
+ onClickOutside,
+ loading,
+ overlayClassName
+}) => {
+ const popperDropdown = useRef(null)
+ const [dropdownData, setDropdownData] = useState(data)
+
+ useEffect(() => {
+ setDropdownData(data)
+ }, [data])
+
+ const highlightKeyword = useCallback(
+ (title, uniqueKey) => {
+ const searchbarValue = String(inputVal)
+ let keyword = inputVal
+ keyword = searchbarValue.includes('[') ? keyword.replace(/\[/gi, '\\[') : keyword
+ keyword = searchbarValue.includes('(') ? keyword.replace(/\(/gi, '\\(') : keyword
+ keyword = searchbarValue.includes(')') ? keyword.replace(/\)/gi, '\\)') : keyword
+
+ const parts = title.split(new RegExp(`(${keyword})`, 'gi'))
+ return inputVal && inputVal.length > 0 ? (
+
+ {parts.map((part) =>
+ part === searchbarValue ? (
+
+ {part}
+
+ ) : (
+ part
+ )
+ )}
+
+ ) : (
+ title
+ )
+ },
+ [inputVal]
+ )
+ const ItemChildren = useCallback(
+ (item) => {
+ return (
+
+ {item.children &&
+ item.children.map((ele) => {
+ return (
+ -
+ {
+ optionsClick(typeof ele.title === 'string' ? ele.title : ele.id, ele)
+ }}
+ >
+ {typeof ele.title === 'string' ? highlightKeyword(ele.title, ele.id) : ele.title}
+
+
+ )
+ })}
+
+ )
+ },
+ [highlightKeyword]
+ )
+
+ const DataSourceRender = useCallback(
+ (item) => {
+ const className = classNames(`${prefixCls}_dropdown--item_normal`, {
+ [`${prefixCls}_dropdown--item-title`]: item.children
+ })
+ return (
+ -
+ {
+ optionsClick(typeof item.title === 'string' ? item.title : item.id, item)
+ }}
+ >
+ {typeof item.title === 'string' ? highlightKeyword(item.title, item.id) : item.title}
+
+ {item.children && ItemChildren(item)}
+
+ )
+ },
+ [highlightKeyword, ItemChildren]
+ )
+
+ const { searchEmptyResult } = localeDatas.search
+ return (
+
+
+
+
+ {dropdownData && dropdownData.length > 0 ? (
+ dropdownData.map((item) => {
+ return DataSourceRender(item)
+ })
+ ) : (
+ - {searchEmptyResult}
+ )}
+
+
+
+
+ )
+}
+
+export default SearchDropdown
diff --git a/components/search/style/index.js b/components/search/style/index.js
new file mode 100644
index 000000000..63810a681
--- /dev/null
+++ b/components/search/style/index.js
@@ -0,0 +1 @@
+import './index.scss'
diff --git a/components/search/style/index.scss b/components/search/style/index.scss
new file mode 100644
index 000000000..a31b74f85
--- /dev/null
+++ b/components/search/style/index.scss
@@ -0,0 +1,127 @@
+@import '../../core-css/index.scss';
+$search: 'hi-search' !default;
+
+.#{$search} {
+ display: inline-block;
+
+ &_input {
+ display: flex;
+
+ .hi-input__inner {
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+
+ &:hover {
+ .hi-btn {
+ border-left-color: transparent;
+ }
+ }
+ }
+
+ .hi-btn {
+ border-left-color: transparent;
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+
+ &:hover:not(.disabled) {
+ border-color: use-color('primary');
+ }
+
+ &:focus {
+ box-shadow: none;
+ }
+ }
+ }
+
+ .hi-icon.icon-search {
+ color: use-color('primary');
+ }
+
+ &_dropdown {
+ ul {
+ margin: 0;
+ padding: 0;
+ }
+
+ li {
+ margin: 0;
+ list-style: none;
+ }
+
+ p {
+ margin: 0;
+ padding: 0;
+ }
+
+ width: 100%;
+ height: 252px;
+ font-size: 12px;
+ color: use-color('black');
+ overflow-x: hidden;
+ overflow-y: auto;
+ background-color: use-color('white');
+ box-shadow: 0 1px 6px rgba(0, 0, 0, 0.2);
+ border-radius: 2px;
+ box-sizing: border-box;
+ text-align: left;
+ margin-top: -4px;
+
+ &--item {
+ flex: none;
+ display: block;
+ line-height: 36px;
+ margin: 0;
+ font-weight: 400;
+ cursor: pointer;
+ list-style: none;
+ transition: all 0.3s ease;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+
+ &_normal {
+ display: block;
+ line-height: 36px;
+ padding: 0 12px;
+
+ &:hover:not(.hi-search_dropdown--item-title) {
+ background-color: use-color('primary-20');
+ }
+ }
+
+ &-title {
+ color: use-color('gray-50');
+ font-size: 14px;
+ }
+
+ &-history {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 0 12px;
+ color: use-color('gray-50');
+ font-size: 14px;
+ }
+
+ &-nodata {
+ height: 260px;
+ line-height: 260px;
+ text-align: center;
+ color: use-color('gray-50');
+ font-size: 14px;
+ }
+
+ &__name-hightlight {
+ color: use-color('primary');
+ }
+ }
+
+ &--more {
+ line-height: 36px;
+ margin: 0;
+ font-weight: 400;
+ cursor: pointer;
+ font-size: 12px;
+ text-align: center;
+ }
+ }
+}
diff --git a/components/select-tree/SelectTreeHook.js b/components/select-tree/SelectTreeHook.js
new file mode 100644
index 000000000..806ca4c4a
--- /dev/null
+++ b/components/select-tree/SelectTreeHook.js
@@ -0,0 +1,441 @@
+import React, { useState, useRef, useCallback, useEffect } from 'react'
+import Tree from './components/tree'
+import _ from 'lodash'
+import Popper from '../popper'
+import HiRequest from '../_util/hi-request'
+import Icon from '../icon'
+
+import {
+ flattenNodesData,
+ getNode,
+ updateCheckData,
+ updateUnCheckData,
+ hasChildren,
+ parseDefaultSelectedItems,
+ parseCheckStatusData,
+ parseSelectedItems,
+ treeFilterByOriginalData,
+ parseExpandIds,
+ fillNodeEntries,
+ clearReturnData,
+ processSelectedIds
+} from './components/tree/util'
+import NavTree from './components/tree/NavTree'
+import Trigger from './components/Trigger'
+import Provider from '../context'
+
+const SelectTree = ({
+ data,
+ dataSource,
+ type,
+ defaultExpandIds,
+ expandIds: expandIdsProps,
+ defaultExpandAll,
+ showCheckedMode,
+ defaultValue,
+ value,
+ onChange,
+ onExpand,
+ defaultLoadData,
+ clearable,
+ searchMode,
+ mode,
+ autoExpand,
+ overlayClassName,
+ theme,
+ localeDatas
+}) => {
+ const selectedItemsRef = useRef()
+ const inputRef = useRef()
+ // select 中显示的数量
+ const [showCount, setShowCount] = useState(1)
+ // panel show
+ const [show, setShow] = useState(false)
+ // panel status : empty normal loading
+ const [nodeDataState, setNodeDataState] = useState('normal')
+ // selected items
+ const [selectedItems, setSelectedItems] = useState([])
+ // expand ids
+ const [expandIds, setExpandIds] = useState([])
+ // 改造过后的数据
+ const [nodeEntries, setNodeEntries] = useState({})
+ // 选中的节点数据(checkable)
+ const [checkedNodes, setCheckedNodes] = useState({
+ checked: [],
+ semiChecked: []
+ })
+ // 拉平的数据
+ const [flattenData, setFlattenData] = useState([])
+ // 关键字搜索值
+ const [searchValue, setSearchValue] = useState('')
+ // 清空搜索值事件
+ const clearSearchEvent = useCallback(() => {
+ setSearchValue('')
+ searchTreeNode('')
+ }, [flattenData])
+
+ // 搜索框的值改变时的事件
+ const changeEvents = useCallback(
+ (e) => {
+ const val = e.target.value
+ setSearchValue(val)
+ searchTreeNode(val)
+ },
+ [flattenData]
+ )
+ // 根据传入的原始数据解析为拉平数据及改装数据
+ useEffect(() => {
+ setStatus()
+ if (data) {
+ const result = flattenNodesData(
+ data,
+ defaultExpandIds,
+ defaultExpandAll,
+ type === 'multiple' && showCheckedMode !== 'ALL'
+ )
+ setFlattenData(result.flattenData)
+ setNodeEntries(result.nodeEntries)
+ }
+ }, [data])
+ // 依赖 flattenData & value 解析生成 checkedNodes 或 selectedItems
+ useEffect(() => {
+ if (flattenData.length > 0) {
+ if (type === 'multiple') {
+ const cstatus = parseCheckStatusData(defaultValue.length > 0 ? defaultValue : value, checkedNodes, flattenData)
+ if (cstatus) {
+ setCheckedNodes(cstatus)
+ }
+ } else {
+ const _selectedItems = parseDefaultSelectedItems(defaultValue.length > 0 ? defaultValue : value, flattenData)
+ setSelectedItems(_selectedItems)
+ }
+ }
+ }, [value, flattenData])
+ // 当选中项发生改变时(以及首次解析完成默认值后),更改选中项
+ useEffect(() => {
+ const _selectedItems = parseSelectedItems(checkedNodes, nodeEntries, showCheckedMode, flattenData)
+ setSelectedItems(_selectedItems)
+ }, [checkedNodes])
+
+ // 依赖展开项生成展开节点数据
+ useEffect(() => {
+ if (flattenData.length > 0) {
+ const _expandIds = parseExpandIds(expandIdsProps, defaultExpandIds, flattenData)
+ setExpandIds((preExpandIds) => {
+ return (_expandIds || []).concat(preExpandIds || [])
+ })
+ }
+ }, [expandIdsProps, flattenData])
+
+ useEffect(() => {
+ if (selectedItemsRef.current) {
+ const sref = selectedItemsRef.current
+ // 多选超过一行时以数字显示
+ const referenceEls = sref.querySelectorAll('.hi-selecttree__selected--hidden span')
+ const wrapperWidth = sref.getBoundingClientRect()
+ let _width = 0
+ let num = 0
+ for (const el of referenceEls) {
+ const elRect = el.getBoundingClientRect()
+ _width += elRect.width
+ // 16 -- '+1' width
+ if (_width + 16 > wrapperWidth.width) {
+ break
+ }
+ num++
+ }
+ setShowCount(num)
+ }
+ }, [selectedItems])
+
+ // 过滤方法
+ const searchTreeNode = (val) => {
+ const matchNodes = []
+ const _data = _.cloneDeep(flattenData)
+ if (searchMode === 'highlight') {
+ const filterArr = _data.map((node) => {
+ let _keyword = val
+ _keyword = val.includes('[') ? _keyword.replace(/\[/gi, '\\[') : _keyword
+ _keyword = val.includes('(') ? _keyword.replace(/\(/gi, '\\(') : _keyword
+ _keyword = val.includes(')') ? _keyword.replace(/\)/gi, '\\)') : _keyword
+ const reg = new RegExp(_keyword, 'gi')
+ const str = `${val}`
+ if (reg.test(node.title)) {
+ node._title = node.title.replace(reg, str)
+ matchNodes.push(node)
+ }
+ return node
+ })
+ setFlattenData(filterArr)
+ let matchNodesSet = []
+ matchNodes.forEach((mn) => {
+ matchNodesSet.push(mn.id)
+ matchNodesSet = matchNodesSet.concat(mn.ancestors || [])
+ })
+ matchNodesSet = _.uniq(matchNodesSet)
+ setExpandIds(matchNodesSet)
+ } else if (searchMode === 'filter') {
+ const filterArr = treeFilterByOriginalData(data, val)
+ const filterData = flattenNodesData(filterArr).flattenData
+ let matchNodesSet = []
+ filterData.forEach((mn) => {
+ matchNodesSet.push(mn.id)
+ matchNodesSet = matchNodesSet.concat(mn.ancestors || [])
+ })
+ matchNodesSet = _.uniq(matchNodesSet)
+ setExpandIds(matchNodesSet)
+ setFlattenData(filterData)
+ }
+ }
+
+ /**
+ * Clear button Event
+ */
+ const handleClear = useCallback(() => {
+ setSelectedItems([])
+ setExpandIds([])
+ setCheckedNodes({
+ checked: [],
+ semiChecked: []
+ })
+ }, [])
+
+ /**
+ * set Pull Data status
+ */
+ const setStatus = useCallback(() => {
+ if (data.length === 0) {
+ setNodeDataState(dataSource ? 'loading' : 'empty')
+ } else {
+ setNodeDataState('normal')
+ }
+ }, [data])
+ /**
+ * Remote load Data
+ * @param {*} id click node's id
+ */
+ const loadNodes = useCallback((id) => {
+ const _dataSource = typeof dataSource === 'function' ? dataSource(id || '') : dataSource
+ return HiRequest({
+ ..._dataSource
+ }).then((res) => {
+ const nArr = res.data.map((n) => {
+ return {
+ ...n,
+ pId: id
+ }
+ })
+ return nArr
+ })
+ }, [])
+ /**
+ * 多选模式下复选框事件
+ * @param {*} checked 是否被选中
+ * @param {*} node 当前节点
+ */
+ const checkedEvents = (checked, node) => {
+ let result = {}
+ const semiCheckedIds = new Set(checkedNodes.semiChecked)
+ const checkedIds = new Set(checkedNodes.checked)
+ if (checked) {
+ result = updateCheckData(node, flattenData, checkedIds, semiCheckedIds)
+ } else {
+ result = updateUnCheckData(node, flattenData, checkedIds, semiCheckedIds)
+ }
+ setCheckedNodes({
+ checked: result.checked,
+ semiChecked: result.semiChecked
+ })
+ let checkedArr = []
+ if (result.checked.length > 0) {
+ checkedArr = result.checked.map((id) => {
+ return getNode(id, flattenData)
+ })
+ }
+ onChange(
+ processSelectedIds(result.checked, nodeEntries, showCheckedMode, flattenData),
+ clearReturnData(checkedArr),
+ clearReturnData(node)
+ )
+ }
+
+ /**
+ * 节点展开关闭事件
+ * @param {*} bol 是否展开
+ * @param {*} node 当前点击节点
+ */
+ const expandEvents = (node, state, callback = () => {}) => {
+ const _expandIds = [...expandIds]
+ const hasIndex = _expandIds.findIndex((id) => id === node.id)
+ if (hasIndex !== -1) {
+ _expandIds.splice(hasIndex, 1)
+ } else {
+ _expandIds.push(node.id)
+ }
+ setExpandIds(_expandIds)
+ if (hasChildren(node, flattenData)) {
+ // 如果包含节点,则不再拉取数据
+ callback()
+ onExpand()
+ return
+ }
+ if (state) {
+ loadNodes(node.id).then((res) => {
+ if (res.length > 0) {
+ setFlattenData(flattenData.concat(flattenNodesData(res).flattenData))
+ fillNodeEntries(node, nodeEntries, res)
+ }
+ // callback(res)
+ })
+ onExpand()
+ }
+ }
+
+ /**
+ * Node selected Event
+ */
+ const selectedEvents = useCallback((node) => {
+ setSelectedItems([node])
+ const n = clearReturnData(node)
+ onChange(node.id, n, n)
+ setShow(false)
+ }, [])
+
+ /**
+ * Input 点击事件
+ */
+ const onTrigger = () => {
+ if (flattenData.length === 0 && defaultLoadData && (!data || data.length === 0 || dataSource) && !show) {
+ // data 为空 且 存在 dataSource 时,默认进行首次数据加载.defaultLoadData不暴露
+ setNodeDataState('loading')
+ loadNodes()
+ .then((res) => {
+ if (res.length === 0) {
+ setNodeDataState('empty')
+ return
+ }
+ setNodeDataState('normal')
+ setFlattenData(flattenNodesData(res).flattenData)
+ fillNodeEntries(null, nodeEntries, res)
+ })
+ .catch(() => {
+ setNodeDataState('empty')
+ })
+ }
+ setShow(!show)
+ }
+ const searchable = searchMode === 'filter' || searchMode === 'highlight'
+ return (
+
+
+ {
+
setShow(false)}
+ >
+
+ {searchable && mode !== 'breadcrumb' && (
+
+
+
+ {
+ if (e.keyCode === '13') {
+ searchTreeNode(e.target.value)
+ }
+ }}
+ onChange={changeEvents}
+ />
+ {searchValue.length > 0 ? (
+
+ ) : null}
+
+
+ )}
+ {mode === 'breadcrumb' ? (
+
+ ) : (
+
+ )}
+
+
+ }
+ {/*
*/}
+
+ )
+}
+
+SelectTree.defaultProps = {
+ type: 'single',
+ defaultCheckedIds: [],
+ defaultValue: [],
+ value: [],
+ data: [],
+ clearable: true,
+ onChange: () => {},
+ onExpand: () => {},
+ checkable: false,
+ defaultLoadData: true,
+ showCheckedMode: 'PARENT',
+ defaultExpandAll: false,
+ defaultExpandIds: [],
+ expandIds: [],
+ mode: 'tree',
+ searchMode: null
+}
+export default Provider(SelectTree)
diff --git a/components/select-tree/components/Trigger.jsx b/components/select-tree/components/Trigger.jsx
new file mode 100644
index 000000000..50a9f5615
--- /dev/null
+++ b/components/select-tree/components/Trigger.jsx
@@ -0,0 +1,72 @@
+import React from 'react'
+import classNames from 'classnames'
+
+const Trigger = ({
+ inputRef,
+ type,
+ selectedItems,
+ onTrigger,
+ checkedEvents,
+ onClear,
+ showCount,
+ clearable,
+ show,
+ selectedItemsRef
+}) => {
+ return (
+
+
+
+ {selectedItems.map((node, index) => (
+ {node.title || ''}
+ ))}
+
+ {selectedItems.length > 0 &&
+ selectedItems.slice(0, showCount || 1).map((node, index) => {
+ return (
+
+
{node ? node.title : ''}
+ {type === 'multiple' && (
+
{
+ e.stopPropagation()
+ checkedEvents(false, node)
+ }}
+ >
+
+
+ )}
+
+ )
+ })}
+ {!!showCount && showCount < selectedItems.length && (
+
+ +{selectedItems.length - showCount}
+
+ )}
+
+
+
+ 0
+ })}
+ />
+ {clearable && selectedItems.length > 0 && (
+
+ )}
+
+
+ )
+}
+
+export default Trigger
diff --git a/components/select-tree/components/tree/LoadingIcon.jsx b/components/select-tree/components/tree/LoadingIcon.jsx
new file mode 100644
index 000000000..84346c230
--- /dev/null
+++ b/components/select-tree/components/tree/LoadingIcon.jsx
@@ -0,0 +1,32 @@
+import React from 'react'
+
+const IconLoading = (props) => {
+ const size = '0.8em'
+ const themeColor = {
+ orange: '#f63',
+ cyan: '#46bc99',
+ magenta: '#ff5975',
+ lavender: '#b450de',
+ blue: '#3da8f5',
+ purple: '#8a8acb'
+ }
+ return (
+
+
+
+ )
+}
+export default IconLoading
diff --git a/components/select-tree/components/tree/NavTree.js b/components/select-tree/components/tree/NavTree.js
new file mode 100644
index 000000000..9a17d7051
--- /dev/null
+++ b/components/select-tree/components/tree/NavTree.js
@@ -0,0 +1,155 @@
+import React, { useState, useRef, useEffect, useCallback } from 'react'
+import Icon from '../../../icon'
+import { getRootNodes, getChildrenNodes } from './util'
+import classNames from 'classnames'
+import Loading from '../../../loading'
+import Checkbox from '../../../checkbox'
+
+const Bread = ({ datas, onClick, onReturnClick, localeDatas }) => {
+ const datasArr = Object.keys(datas)
+ datasArr.unshift(localeDatas.selectTree.back)
+ if (datasArr.length > 3) {
+ datasArr.splice(1, datasArr.length - 3, '...')
+ }
+ return (
+
+ {datasArr.map((item, index) => {
+ const cls = classNames(
+ 'hi-breadtree__bread-content',
+ datasArr.length > 3 && index === 1 && 'hi-breadtree__bread-content--normal'
+ )
+ return (
+
+ {
+ index === 0
+ ? onReturnClick()
+ : ((datasArr.length === 4 && index !== 1) || datasArr.length < 4) && onClick(item)
+ }}
+ >
+ {item}
+
+ {index < datasArr.length - 1 && }
+
+ )
+ })}
+
+ )
+}
+const NavTree = ({
+ data,
+ checkable,
+ checkedNodes,
+ onCheck,
+ onSelected,
+ selectedItems,
+ autoExpand = true,
+ nodeDataState,
+ onExpand: expandProps,
+ localeDatas
+}) => {
+ const expandData = useRef()
+ const [renderData, setRenderData] = useState([])
+ const [fullBreadData, setFullBreadData] = useState({})
+ const [loadingState, setLoadingState] = useState('normal')
+ const [currentNode, setCurrentNode] = useState(null)
+
+ useEffect(() => {
+ setLoadingState(nodeDataState)
+ }, [nodeDataState])
+
+ useEffect(() => {
+ const roots = currentNode ? getChildrenNodes(currentNode, data) : getRootNodes(data)
+ setRenderData(roots)
+ }, [data])
+ const onBreadClick = (title) => {
+ const node = fullBreadData[title]
+ setRenderData(getChildrenNodes(node, data))
+ setFullBreadData((preData) => {
+ const keysArr = Object.keys(preData)
+ const delArr = keysArr.filter((_, index) => index > keysArr.indexOf(title))
+ delArr.forEach((key) => {
+ delete preData[key]
+ })
+ return preData
+ })
+ }
+ const onReturnClick = useCallback(() => {
+ setLoadingState('normal')
+ setRenderData(getRootNodes(data))
+ setFullBreadData({})
+ })
+ const onNodeClick = (node, children) => {
+ if (node.isLeaf) {
+ onSelected(node)
+ return
+ }
+ expandData.current = expandData.current ? expandData.current.concat([node]) : [].concat([node])
+ setFullBreadData((preData) => {
+ return {
+ ...preData,
+ [node.title]: node
+ }
+ })
+ setCurrentNode(node)
+ if (children.length > 0) {
+ setRenderData(children)
+ } else {
+ setRenderData([])
+ setLoadingState('loading')
+ expandProps(node, true, (arg) => {
+ setLoadingState(arg.length > 0 ? 'normal' : 'empty')
+ })
+ }
+ }
+ return (
+
+ {Object.keys(fullBreadData).length > 0 && (
+
+ )}
+ {loadingState === 'loading' &&
}
+
+ {loadingState === 'empty' ? (
+ -
+ empty
+
+ ) : (
+ renderData.map((node, index) => {
+ const children = getChildrenNodes(node, data)
+ const textCls = classNames(
+ 'hi-breadtree__text',
+ selectedItems.find((n) => n.id === node.id) && 'hi-breadtree__text--selected'
+ )
+ return (
+ -
+ {checkable && node.isLeaf ? (
+ onCheck(e.target.checked, node)}
+ >
+ {node.title}
+
+ ) : (
+ {
+ onNodeClick(node, children)
+ }}
+ >
+ {node.title}
+
+ )}
+ {(children.length > 0 || !node.isLeaf) && (
+ onNodeClick(node, children)} />
+ )}
+
+ )
+ })
+ )}
+
+
+ )
+}
+export default NavTree
diff --git a/components/select-tree/components/tree/TreeNode.jsx b/components/select-tree/components/tree/TreeNode.jsx
new file mode 100644
index 000000000..8611980eb
--- /dev/null
+++ b/components/select-tree/components/tree/TreeNode.jsx
@@ -0,0 +1,105 @@
+import React, { useContext, useRef, useCallback, useState } from 'react'
+import Checkbox from '../../../checkbox'
+import Icon from '../../../icon'
+import Classnames from 'classnames'
+import TreeContext from './context'
+import IconLoading from './LoadingIcon'
+import { getChildrenNodes } from './util'
+
+const Switcher = ({ expanded, node, onExpandEvent }) => {
+ const [loading, setLoading] = useState(false)
+ return loading ? (
+
+ ) : (
+ {
+ !expanded && setLoading(true)
+ onExpandEvent(node, !expanded, () => setLoading(false))
+ }}
+ />
+ )
+}
+
+const TreeNode = ({ data, flttenData }) => {
+ // 接受原始拉平数据,用于快速查找子元素
+ const {
+ treeNodeRender,
+ checkable,
+ checkedNodes,
+ selectedItems,
+ onClick,
+ selectedId,
+ onCheckboxChange,
+ expandIds,
+ onExpandEvent,
+ isRemoteLoadData
+ } = useContext(TreeContext)
+ const treeNodeRef = useRef(null)
+
+ const renderIndent = useCallback(() => {
+ return
+ }, [])
+
+ const renderCheckbox = useCallback(
+ (node, { checked, semiChecked }) => {
+ return (
+ {
+ onCheckboxChange(e.target.checked, node, { checked, semiChecked })
+ }}
+ >
+
+
+ )
+ },
+ [checkedNodes]
+ )
+
+ const renderTitle = useCallback((node, _selectedId) => {
+ const { id, title, _title } = node
+ return (
+ s.id === id).length > 0
+ })}
+ onClick={() => {
+ onClick(node)
+ }}
+ dangerouslySetInnerHTML={{ __html: treeNodeRender ? treeNodeRender(_title || title) : _title || title }}
+ />
+ )
+ }, [])
+ return (
+
+ {data.map((node, index) => {
+ const childrenNodes = getChildrenNodes(node, flttenData)
+ const expand = expandIds.includes(node.id)
+ return (
+
+ -
+
+ {(childrenNodes.length || isRemoteLoadData) && !node.isLeaf ? (
+
+ ) : (
+ renderIndent()
+ )}
+ {checkable ? renderCheckbox(node, checkedNodes) : renderTitle(node, selectedId)}
+
+
+ {childrenNodes.length > 0 && expand && (
+ -
+
+
+ )}
+
+ )
+ })}
+
+ )
+}
+
+export default TreeNode
diff --git a/components/select-tree/components/tree/context.jsx b/components/select-tree/components/tree/context.jsx
new file mode 100644
index 000000000..056b3bcc2
--- /dev/null
+++ b/components/select-tree/components/tree/context.jsx
@@ -0,0 +1,5 @@
+import React from 'react'
+
+const TreeContext = React.createContext()
+
+export default TreeContext
diff --git a/components/select-tree/components/tree/index.jsx b/components/select-tree/components/tree/index.jsx
new file mode 100644
index 000000000..72b6327c3
--- /dev/null
+++ b/components/select-tree/components/tree/index.jsx
@@ -0,0 +1,57 @@
+import React from 'react'
+import Loading from '../../../loading'
+import TreeNode from './TreeNode'
+import TreeContext from './context'
+import './style/index'
+import { getRootNodes } from './util'
+
+const PREFIX = 'hi-select-tree'
+
+const Tree = ({
+ data,
+ expandIds,
+ checkedNodes,
+ selectedItems,
+ treeNodeRender,
+ checkable,
+ editable,
+ editMenu,
+ onClick,
+ onCheck,
+ onExpand,
+ nodeDataState,
+ loadDataOnExpand,
+ isRemoteLoadData
+}) => {
+ return (
+
+
+ {nodeDataState === 'loading' && }
+ {nodeDataState === 'empty' && empty}
+ {nodeDataState === 'normal' && }
+
+
+ )
+}
+
+Tree.defaultProps = {
+ onExpand: () => {},
+ loadDataOnExpand: false
+}
+export default Tree
diff --git a/components/select-tree/components/tree/style/index.js b/components/select-tree/components/tree/style/index.js
new file mode 100644
index 000000000..63810a681
--- /dev/null
+++ b/components/select-tree/components/tree/style/index.js
@@ -0,0 +1 @@
+import './index.scss'
diff --git a/components/select-tree/components/tree/style/index.scss b/components/select-tree/components/tree/style/index.scss
new file mode 100644
index 000000000..5867d61a1
--- /dev/null
+++ b/components/select-tree/components/tree/style/index.scss
@@ -0,0 +1,144 @@
+@import '../../../../core-css/index.scss';
+$tree: 'hi-select-tree' !default;
+
+.#{$tree} {
+ color: use-color('black');
+ padding: 12px 12px 0 12px;
+ box-sizing: border-box;
+ height: 100%;
+ overflow: auto;
+
+ &--empty {
+ margin-left: 50%;
+ margin-top: 45%;
+ display: inline-block;
+ transform: translate(-50%, 0);
+ }
+
+ &__nodes {
+ list-style-type: none;
+ padding: 0;
+ margin: 0;
+ }
+
+ &__node {
+ .hi-btn--loading--icon {
+ margin-top: -8px;
+ }
+
+ ul {
+ padding: 0 16px;
+ }
+
+ &--self {
+ display: flex;
+ align-items: center;
+ cursor: pointer;
+ margin-bottom: 12px;
+
+ .hi-checkbox {
+ white-space: nowrap;
+ }
+
+ .icon-check {
+ color: use-color('primary');
+ }
+ }
+
+ .hi-icon {
+ cursor: pointer;
+ font-size: 16px;
+ margin-right: 4px;
+ }
+ }
+
+ &__title {
+ height: 24px;
+ line-height: 24px;
+ text-align: left;
+ white-space: nowrap;
+ padding: 0 4px;
+ border-radius: 2px;
+
+ &:hover {
+ color: #383e47;
+ background-color: use-color('primary-10');
+ }
+
+ &--selected {
+ color: use-color('primary');
+
+ &:hover {
+ color: use-color('primary');
+ }
+ }
+ }
+}
+
+.hi-breadtree {
+ &__root {
+ height: 100%;
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ }
+
+ &__list {
+ margin: 0;
+ padding: 0 12px;
+ }
+
+ &__item {
+ list-style: none;
+ height: 36px;
+ display: flex;
+ align-items: center;
+ cursor: pointer;
+ }
+
+ &__text {
+ flex: 1;
+
+ &--selected {
+ color: use-color('primary');
+ }
+ }
+
+ &__bread {
+ display: flex;
+ flex-wrap: nowrap;
+ justify-content: flex-start;
+ height: 36px;
+ align-items: center;
+ padding: 0 12px;
+ background: rgba(247, 247, 247, 1);
+ font-size: 14px;
+ flex: none;
+ }
+
+ &__separator {
+ margin: 2px 4px 0;
+ }
+
+ &__bread-content {
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ margin: 0 4px;
+
+ &:first-child {
+ cursor: pointer;
+ margin-left: 0;
+ flex: 0 0 auto;
+ }
+
+ &:hover:not(:last-child) {
+ color: use-color('primary');
+ cursor: pointer;
+ }
+
+ &--normal {
+ flex: 0 0 auto;
+ }
+ }
+}
diff --git a/components/select-tree/components/tree/util.js b/components/select-tree/components/tree/util.js
new file mode 100644
index 000000000..0ef85bbf1
--- /dev/null
+++ b/components/select-tree/components/tree/util.js
@@ -0,0 +1,364 @@
+import _ from 'lodash'
+// 根据 ID 获取节点
+export const getNode = (id, data) => {
+ return data.find((n) => n.id === id)
+}
+// 根据 title 获取节点
+export const getNodeByTitle = (title, data) => {
+ return data.find((n) => n.title === title)
+}
+// 根据 id || title 获取节点
+export const getNodeByIdTitle = (val, data) => {
+ return data.find((n) => n.title === val || n.id === val)
+}
+// 获取指定节点的兄弟节点
+export const getSibilingsNodes = (node, data) => {
+ return data.filter((n) => n.pId === node.pId)
+}
+
+// 获取指定节点的父节点
+export const getParentNode = (node, data) => {
+ return data.find((n) => n.id === node.pId)
+}
+// 获取指定节点的直接子节点
+export const getChildrenNodes = (node, data) => {
+ return data.filter((n) => n.pId === node.id)
+}
+// 获取根节点
+export const getRootNodes = (data) => {
+ return data.filter((n) => !n.pId)
+}
+// 获取指定节点的所有后代节点
+export const getDescendantNodes = (node, data, arr = []) => {
+ const children = getChildrenNodes(node, data)
+ if (children.length > 0) {
+ children.forEach((n) => {
+ arr.push(n)
+ if (!n.isLeaf) {
+ getDescendantNodes(n, data, arr)
+ }
+ })
+ }
+ return arr
+}
+
+export const hasChildren = (node, data) => {
+ let bol = false
+ for (let i = 0; i < data.length; i++) {
+ if (data[i].pId === node.id) {
+ bol = true
+ break
+ }
+ }
+ return bol
+}
+// 移除指定节点的所有后代节点
+export const removeDescendantNodes = (node, data) => {
+ const descs = getDescendantNodes(node, data)
+ descs.map((d) => d.id)
+ data.filter((n) => descs.forEach())
+}
+// 获取指定节点的所有祖先节点
+export const getAncestorsNodes = (node, data, arr = []) => {
+ const firstParentNode = getParentNode(node, data)
+ if (firstParentNode) {
+ arr.push(firstParentNode)
+ getAncestorsNodes(firstParentNode, data, arr)
+ }
+ return arr
+}
+
+/**
+ * 处理选中数据
+ * @param {*} node 当前节点
+ * @param {*} data 拉平数据
+ * @param {*} checkedIds 选中 IDS
+ * @param {*} semiCheckedIds 半选 IDS
+ */
+export const updateCheckData = (node, data, checkedIds, semiCheckedIds) => {
+ const children = getDescendantNodes(node, data)
+ const ancestors = node.pId ? getAncestorsNodes(node, data) : []
+ children.forEach((child) => {
+ checkedIds.add(child.id)
+ semiCheckedIds.delete(child.id)
+ })
+ semiCheckedIds.delete(node.id)
+ checkedIds.add(node.id)
+ ancestors.forEach((ancestor) => {
+ const chi = getChildrenNodes(ancestor, data).map((c) => c.id)
+ const ins = _.intersection(chi, [...checkedIds])
+ if (ins.length === chi.length) {
+ checkedIds.add(ancestor.id)
+ semiCheckedIds.delete(ancestor.id)
+ } else {
+ semiCheckedIds.add(ancestor.id)
+ }
+ })
+ return {
+ checked: [...checkedIds],
+ semiChecked: [...semiCheckedIds]
+ }
+}
+
+/**
+ * 处理取消选中数据
+ * @param {*} node 当前节点
+ * @param {*} data 拉平数据
+ * @param {*} checkedIds 选中 IDS
+ * @param {*} semiCheckedIds 半选 IDS
+ */
+export const updateUnCheckData = (node, data, checkedIds, semiCheckedIds) => {
+ const children = getDescendantNodes(node, data)
+ const ancestors = node.pId ? getAncestorsNodes(node, data) : []
+ checkedIds.delete(node.id)
+ ancestors.forEach((ancestor) => {
+ checkedIds.delete(ancestor.id)
+ semiCheckedIds.add(ancestor.id)
+ const chi = getChildrenNodes(ancestor, data).map((c) => c.id)
+ const ins = _.intersection(chi, [...checkedIds, ...semiCheckedIds])
+ if (ins.length === 0) {
+ checkedIds.delete(ancestor.id)
+ semiCheckedIds.delete(ancestor.id)
+ }
+ })
+
+ children.forEach((child) => {
+ checkedIds.delete(child.id)
+ })
+ return {
+ checked: [...checkedIds],
+ semiChecked: [...semiCheckedIds]
+ }
+}
+
+/**
+ * 处理选中的回显数据
+ * @param {*} checkedIds 当前所有被选中的节点 ID 集合
+ * @param {*} nodeEntries 所有数据的Map 集合
+ * @param {*} type 数据回显方式
+ */
+export const processSelectedIds = (checkedIds, nodeEntries, type, flattenData) => {
+ const keySet = new Set(checkedIds)
+ if (type === 'CHILD') {
+ return checkedIds.filter((id) => {
+ const entity = nodeEntries[id]
+ if (entity) {
+ let children = []
+ if (entity.children && entity.children.length > 0) {
+ children = entity.children
+ } else {
+ // 当异步加载数据后,集合中不存在 children,根据节点取 children
+ children = getChildrenNodes(entity, flattenData)
+ }
+ if (children.length === 0) {
+ return true
+ }
+ if (children.every((node) => keySet.has(node.id))) {
+ return false
+ }
+ }
+ return true
+ })
+ }
+ if (type === 'PARENT') {
+ return checkedIds.filter((id) => {
+ const entity = nodeEntries[id]
+ const parent = entity ? entity.parent : null
+ if (parent && keySet.has(parent.id)) {
+ return false
+ }
+ return true
+ })
+ }
+ return checkedIds
+}
+
+/**
+ * 生成展开数据
+ * @param {*} expandIds 受控展开节点 IDS
+ * @param {*} defaultExpandIds 非受控展开节点 IDS
+ * @param {*} flattenData 拉平数据
+ */
+export const parseExpandIds = (expandIds, defaultExpandIds, flattenData) => {
+ const ids = defaultExpandIds.length > 0 ? defaultExpandIds : expandIds
+ let arr = []
+ ids.forEach((id) => {
+ const node = getNode(id, flattenData)
+ if (node) {
+ arr.push(node.id)
+ node.ancestors && node.ancestors.length > 0 && (arr = arr.concat(node.ancestors))
+ }
+ })
+ return [...new Set(arr)]
+}
+/**
+ * 将数据拉平为 pId 类数据
+ * @param {*} data 原始数据
+ * @param {*} defaultExpandIds 默认展开节点
+ * @param {*} defaultExpandAll 是否默认展开全部节点
+ * @param {*} isGenEntries 是否生成 map 集合(当多选且数据回显方式不等于 ALL 时)
+ */
+export const flattenNodesData = (data, isGenEntries = false) => {
+ const flattenData = []
+ const nodeEntries = {}
+ const fun = (datas, newArr, parent = {}) => {
+ datas = _.cloneDeep(datas)
+ datas.forEach((node) => {
+ const pId = parent.id
+ node.pId = pId
+ if (pId) {
+ const arr = parent.ancestors ? [...parent.ancestors] : []
+ arr.unshift(pId)
+ node.ancestors = arr
+ }
+ const _children = node.children
+ newArr.push(node)
+ isGenEntries &&
+ (nodeEntries[node.id] = {
+ ...node,
+ parent
+ })
+ if (_children) {
+ fun(_children, newArr, node)
+ delete node.children
+ } else {
+ node.isLeaf = node.hasOwnProperty('isLeaf') ? node.isLeaf : true
+ }
+ })
+ }
+ fun(data, flattenData)
+ return {
+ flattenData,
+ nodeEntries
+ }
+}
+
+export const fillNodeEntries = (parentNode, currentNodeEntries, newData) => {
+ newData.forEach((nd) => {
+ nd.parent = parentNode
+ currentNodeEntries[nd.id] = nd
+ })
+ return currentNodeEntries
+}
+/**
+ * 根据 defaultValue 解析默认选中项(自动勾选)
+ * 2020.06.28 暂停「不含在数据中」的默认值,会引起诸多副作用
+ * defaultValue:
+ * [id, ...] | [title, ...] | [{id: ..}] | [{id: ..., title: ...}]
+ * 匹配原则: 如果值不符合 {id, title},会优先从现有数据中匹配 id 或 title,如匹配成功,取 node 做为已选中,如无匹配 则跳过
+ * 如同时包含{id, title},从现有数据中匹配对应数据,如有,取 node 做为已选中,如无匹配,则直接使用该值,与现有数据无关联
+ */
+export const parseDefaultSelectedItems = (defaultValue, flattenData) => {
+ const defaultNodes = []
+ if (typeof defaultValue === 'string' || typeof defaultValue === 'number') {
+ const node = getNodeByIdTitle(defaultValue, flattenData)
+ node && defaultNodes.push(node)
+ } else if (defaultValue instanceof Array) {
+ defaultValue.forEach((val) => {
+ let node
+ if (typeof val !== 'object') {
+ // [0, 'x']
+ node = getNodeByIdTitle(val, flattenData)
+ } else {
+ // if (val.id && val.title) {
+ // // [{id: '', title: ''}]
+ // node = getNode(val.id, flattenData) || val
+ // } else {
+ node = getNodeByIdTitle(val.id || val.title, flattenData)
+ // }
+ }
+ node && defaultNodes.push(node)
+ })
+ }
+ return defaultNodes
+}
+
+/**
+ * 根据 defaultCheckedIds 解析全选/半选数据
+ * @param {*} selectedItems 已选中选项
+ */
+export const parseCheckStatusData = (value, checkedNodes, flattenData) => {
+ value = value.concat(checkedNodes.checked)
+ const selectedItems = parseDefaultSelectedItems([...new Set(value)], flattenData)
+ const semiCheckedIds = new Set(checkedNodes.semiChecked)
+ const checkedIds = new Set(checkedNodes.checked)
+ semiCheckedIds.clear()
+ checkedIds.clear()
+ let isUpdate = false
+ selectedItems.forEach((node) => {
+ isUpdate = true
+ updateCheckData(node, flattenData, checkedIds, semiCheckedIds)
+ })
+ if (isUpdate) {
+ return {
+ checked: [...checkedIds],
+ semiChecked: [...semiCheckedIds]
+ }
+ }
+ return null
+}
+
+/**
+ * 根据数据回显方式设定显示的数据
+ * @param {*} checkIds 当前选中的节点 ID 集合
+ */
+export const parseSelectedItems = (checkedNodes, nodeEntries, showCheckedMode, flattenData) => {
+ const keys = processSelectedIds(checkedNodes.checked, nodeEntries, showCheckedMode, flattenData)
+ return keys.map((id) => getNode(id, flattenData))
+}
+
+/**
+ * 匹配值替换为高亮项
+ * @param {*} val 搜索关键字
+ * @param {*} text 节点 title
+ */
+export const matchFilterKey = (val, text = '') => {
+ const reg = new RegExp(val, 'gi')
+ const str = `
${val}`
+ if (reg.test(text)) {
+ text = text.replace(reg, str)
+ return text
+ }
+ return null
+}
+
+/**
+ * 树节点过滤(根据原始数据)
+ * @param {*} data 原始数据
+ * @param {*} filterVal 过滤值
+ */
+export const treeFilterByOriginalData = (data, filterVal) => {
+ const nodes = _.cloneDeep(data)
+ if (!(nodes && nodes.length)) {
+ return
+ }
+ const newChildren = []
+ for (const node of nodes) {
+ const matchResult = matchFilterKey(filterVal, node.title)
+ if (matchResult) {
+ newChildren.push(node)
+ } else {
+ const subs = treeFilterByOriginalData(node.children, filterVal)
+ if ((subs && subs.length) || matchResult) {
+ node.children = subs
+ newChildren.push(node)
+ }
+ }
+ }
+ return newChildren
+}
+
+export const clearReturnData = (arg) => {
+ arg = _.cloneDeep(arg)
+ if (arg instanceof Array) {
+ arg = arg.map((node) => {
+ delete node.ancestors
+ delete node.pId
+ return node
+ })
+ } else {
+ delete arg.ancestors
+ delete arg.pId
+ }
+ return arg
+}
diff --git a/components/select-tree/index.d.ts b/components/select-tree/index.d.ts
new file mode 100644
index 000000000..26b3b1aab
--- /dev/null
+++ b/components/select-tree/index.d.ts
@@ -0,0 +1,41 @@
+import { CSSProperties } from "react"
+
+type DataItem = {
+ id: string | number
+ title: string
+ children?: DataItem[]
+}
+type DataSource = {
+ url: string
+ type?: 'get' | 'post'
+ data?: object
+ params?: object
+ headers?: object
+ mode?: 'same-origin' | 'cors' | 'no-cors' | 'navigate'
+ transformResponse?: (response: object) => DataItem[]
+}
+type FieldNames = {
+ id?: string
+ title?: string
+ disabled?: string
+ children?: string
+}
+const DataSourFun: (keyword: string) => DataSource
+const FilterOptionFun: (keyword: string, item: DataItem) => boolean
+interface Props {
+ type?: 'single' | 'multiple'
+ data?: DataItem[]
+ showCheckedMode?: 'ALL' | 'PARENT' | 'CHILD'
+ mode?: 'normal' | 'breadcrumb'
+ defaultExpandAll?: boolean
+ defaultExpandIds?: string[] | number[]
+ expandIds?: string[] | number[]
+ dataSource?: DataSource | DataSourFun
+ value?: DataItem[] | string[] | number[]
+ defaultValue?: DataItem[] | string[] | number[]
+ searchMode?: 'highlight' | 'filter'
+ onChange?: (selectedIds: string[], changedItem: DataItem) => void
+ overlayClassName?: string
+}
+declare const SelectTree: React.ComponentType
+export default SelectTree
diff --git a/components/select-tree/index.js b/components/select-tree/index.js
new file mode 100644
index 000000000..8e5662c6c
--- /dev/null
+++ b/components/select-tree/index.js
@@ -0,0 +1,4 @@
+import SelectTreeHook from './SelectTreeHook'
+import './style/index'
+
+export default SelectTreeHook
diff --git a/components/select-tree/style/index.js b/components/select-tree/style/index.js
new file mode 100644
index 000000000..63810a681
--- /dev/null
+++ b/components/select-tree/style/index.js
@@ -0,0 +1 @@
+import './index.scss'
diff --git a/components/select-tree/style/index.scss b/components/select-tree/style/index.scss
new file mode 100644
index 000000000..323a7aa50
--- /dev/null
+++ b/components/select-tree/style/index.scss
@@ -0,0 +1,172 @@
+@import '../../core-css/index.scss';
+
+.hi-selecttree {
+ &__input {
+ width: 320px;
+ height: 32px;
+ line-height: 32px;
+ border: 1px solid use-color('gray-30');
+ box-sizing: border-box;
+ border-radius: 2px;
+ text-align: left;
+ padding: 0 0 0 12px;
+ background-color: use-color('white');
+ color: use-color('black');
+ display: inline-flex;
+ justify-content: space-between;
+
+ &:hover {
+ border: 1px solid use-color('primary');
+
+ .hi-selecttree__input--expand.clearable {
+ display: none;
+ }
+
+ .hi-selecttree__icon-close {
+ display: block;
+ }
+
+ .hi-selecttree__icon-close:hover {
+ color: use-color('black');
+ }
+ }
+
+ &--single {
+ .hi-selecttree__selecteditem {
+ background: none;
+ padding-left: 0;
+ }
+ }
+
+ &--placeholder {
+ color: use-color('gray-50');
+ }
+
+ &-icon {
+ margin: 0 12px;
+ cursor: pointer;
+ }
+ }
+
+ &__selected-wrapper {
+ flex: 1 0 80%;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ display: flex;
+ align-items: center;
+ }
+
+ &__icon-close {
+ color: use-color('gray-50');
+ display: none;
+ }
+
+ &__selected {
+ &--hidden {
+ visibility: hidden;
+ position: absolute;
+ z-index: -1;
+
+ span {
+ padding: 0 16px;
+ }
+ }
+ }
+
+ &__selecteditem {
+ flex: 0 0 auto;
+ position: relative;
+ height: 22px;
+ line-height: 22px;
+ padding: 0 20px 0 10px;
+ margin: 0 2px;
+ overflow: hidden;
+ vertical-align: middle;
+ background-color: use-color('gray-10');
+ border-radius: 2px;
+ text-align: left;
+ box-sizing: border-box;
+
+ &-name {
+ font-size: 13px;
+ display: inline-block;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ max-width: 100%;
+ }
+
+ &-remove {
+ cursor: pointer;
+ display: inline-block;
+ font-size: 12px;
+ position: absolute;
+ top: 0;
+ right: 4px;
+ padding: 0 0 0 8px;
+ }
+ }
+
+ &__popper {
+ background: use-color('white');
+ box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.15);
+ border-radius: 2px;
+ max-height: 300px;
+ overflow: auto;
+ width: 320px;
+
+ &--loading {
+ height: 300px;
+ }
+ }
+
+ &__root {
+ font-size: 14px;
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+ position: relative;
+ }
+
+ &--hassearch {
+ padding: 8px 0 0 0;
+ }
+
+ &__searchbar-wrapper {
+ background-color: use-color('white');
+ width: 100%;
+ padding: 0 12px;
+ box-sizing: border-box;
+ }
+
+ &__searchbar-inner {
+ height: 36px;
+ border-bottom: 1px solid use-color('gray-30');
+ box-sizing: border-box;
+ display: flex;
+ align-items: center;
+ padding: 0 2px;
+
+ .hi-selecttree_searchbar__icon-close {
+ color: use-color('gray-50');
+ cursor: pointer;
+ }
+
+ .hi-selecttree_searchbar__icon-close:hover {
+ color: use-color('black');
+ }
+ }
+
+ &__searchinput {
+ width: 100%;
+ height: 100%;
+ border: none;
+ outline: none;
+ padding: 0 10px;
+ }
+
+ &__panel {
+ padding: 12px;
+ }
+}
diff --git a/components/select/MultipleInput.js b/components/select/MultipleInput.js
new file mode 100644
index 000000000..e5ab25af1
--- /dev/null
+++ b/components/select/MultipleInput.js
@@ -0,0 +1,130 @@
+import React, { useState, useEffect, useRef } from 'react'
+import classNames from 'classnames'
+import _ from 'lodash'
+
+import { transKeys } from './utils'
+
+const MultipleInput = ({
+ placeholder,
+ selectedItems: propsSelectItem,
+ dropdownShow,
+ cacheSelectItem,
+ disabled,
+ searchable,
+ clearable,
+ multipleMode = 'nowrap',
+ onFocus,
+ theme,
+ onBlur,
+ onClick,
+ onClickOption,
+ onClear,
+ handleKeyDown,
+ fieldNames,
+ isFocus
+}) => {
+ const icon = dropdownShow ? 'up' : 'down'
+ const [showCount, setShowCount] = useState(0)
+ const tagWrapperRef = useRef('')
+ const calShowCountFlag = useRef(true) // 在渲染完成进行测试是否展示 +1
+
+ useEffect(() => {
+ if (multipleMode === 'nowrap' && calShowCountFlag.current && tagWrapperRef.current) {
+ // 多选超过一行时以数字显示
+ const tagWrapperRect = tagWrapperRef.current.getBoundingClientRect()
+ let width = 0
+ let showCountIndex = 0 // 在第几个开始显示折行
+ const tags = tagWrapperRef.current.querySelectorAll('.hi-select__input--item')
+ tags.forEach((tag, index) => {
+ const tagRect = tag.getBoundingClientRect()
+ width += tagRect.width
+ if (width + 90 > tagWrapperRect.width) {
+ // 50是留给显示剩余选项的空间
+ calShowCountFlag.current = false
+ showCountIndex = index
+ }
+ })
+ !calShowCountFlag.current && setShowCount(showCountIndex)
+ } else {
+ calShowCountFlag.current = true
+ }
+ })
+
+ const handleClear = (e) => {
+ e.stopPropagation()
+ onClear()
+ }
+ const selectedItems = _.uniqBy(cacheSelectItem.concat(propsSelectItem), transKeys(fieldNames, 'id'))
+ const currentCount = showCount === 0 ? selectedItems.length : showCount
+ return (
+
+ {selectedItems.length === 0 &&
{placeholder}
}
+
+ {selectedItems.slice(0, currentCount).map((item, index) => {
+ const _item = (
+
+
{item[transKeys(fieldNames, 'title')]}
+
{
+ e.stopPropagation()
+ onClickOption(item, 0)
+ }}
+ >
+
+
+
+ )
+ return _item
+ })}
+ {currentCount < selectedItems.length && (
+
+ +{selectedItems.length - currentCount}
+
+ )}
+ {searchable && !disabled && (
+
+
+
+ )}
+
+
+ 0
+ })}
+ />
+ {clearable && selectedItems.length > 0 && (
+
+ )}
+
+
+ )
+}
+export default MultipleInput
diff --git a/components/select/Select.js b/components/select/Select.js
index 0561484af..74e6c1ae1 100644
--- a/components/select/Select.js
+++ b/components/select/Select.js
@@ -1,664 +1,504 @@
-import React, { Component } from 'react'
-import ReactDOM from 'react-dom'
+import React, { useState, useEffect, useRef, forwardRef, useCallback } from 'react'
import classNames from 'classnames'
-import PropTypes from 'prop-types'
-import debounce from 'lodash/debounce'
-import cloneDeep from 'lodash/cloneDeep'
+import _ from 'lodash'
+
import Popper from '../popper'
import SelectInput from './SelectInput'
import SelectDropdown from './SelectDropdown'
import Provider from '../context'
-import fetchJsonp from 'fetch-jsonp'
-import qs from 'qs'
-import _ from 'lodash'
-
-class Select extends Component {
- autoloadFlag = true // 第一次自动加载数据标识
-
- static propTypes = {
- type: PropTypes.oneOf(['single', 'multiple']),
- multipleWrap: PropTypes.oneOf(['wrap', 'nowrap']),
- data: PropTypes.array,
- dataSource: PropTypes.oneOfType([
- PropTypes.object,
- PropTypes.func
- ]),
- defaultValue: PropTypes.oneOfType([
- PropTypes.string,
- PropTypes.array,
- PropTypes.bool,
- PropTypes.number
- ]),
- value: PropTypes.oneOfType([
- PropTypes.string,
- PropTypes.array,
- PropTypes.bool,
- PropTypes.number
- ]),
- showCheckAll: PropTypes.bool,
- autoload: PropTypes.bool,
- withCredentials: PropTypes.bool,
- searchable: PropTypes.bool,
- filterOption: PropTypes.func,
- clearable: PropTypes.bool,
- disabled: PropTypes.bool,
- placeholder: PropTypes.string,
- emptyContent: PropTypes.string,
- optionWidth: PropTypes.number,
- style: PropTypes.object,
- onChange: PropTypes.func,
- render: PropTypes.func,
- open: PropTypes.bool
- }
-
- static defaultProps = {
- data: [],
- type: 'single',
- multipleWrap: 'nowrap',
- disabled: false,
- clearable: true,
- defaultValue: '',
- autoload: false,
- showCheckAll: false,
- open: true,
- withCredentials: false,
- onClick: () => {},
- onBlur: () => {},
- onFocus: () => {}
- }
-
- constructor (props) {
- super(props)
-
- const { data, value, defaultValue } = props
- const dropdownItems = cloneDeep(data)
- const initialValue = value === undefined ? defaultValue : value
- const selectedItems = this.resetSelectedItems(
- initialValue,
- dropdownItems,
- []
+import HiRequest from '../_util/hi-request'
+import { resetSelectedItems, transKeys } from './utils'
+
+const InternalSelect = (props) => {
+ const {
+ data,
+ type,
+ showCheckAll,
+ showJustSelected,
+ className,
+ disabled,
+ clearable,
+ style,
+ children,
+ optionWidth,
+ render,
+ multipleWrap,
+ onBlur,
+ onFocus,
+ dataSource,
+ filterOption,
+ theme,
+ localeDatas,
+ preventOverflow,
+ placement,
+ onChange: propsonChange,
+ value,
+ defaultValue,
+ autoload,
+ searchable: propsSearchable,
+ fieldNames,
+ overlayClassName,
+ setOverlayContainer
+ } = props
+ const selectInputContainer = useRef()
+ const [dropdownItems, setDropdownItems] = useState(data)
+ const [focusedIndex, setFocusedIndex] = useState(0)
+ const [isFocus, setIsFouces] = useState(false)
+ // 存储问题
+ const [cacheSelectItem, setCacheSelectItem] = useState([])
+
+ // value 有可能是0的情况
+ const [selectedItems, setSelectedItems] = useState(
+ resetSelectedItems(value === undefined ? defaultValue : value, _.cloneDeep(data), transKeys(fieldNames, 'id'))
+ )
+
+ const [dropdownShow, setDropdownShow] = useState(false)
+ // 搜索关键字
+ const [keyword, setKeyword] = useState('')
+ const [loading, setLoading] = useState(false)
+ const [searchable, setSearchable] = useState(dataSource ? true : propsSearchable)
+ useEffect(() => {
+ // 处理默认值的问题
+ const selectedItems = resetSelectedItems(
+ value === undefined ? defaultValue : value,
+ _.uniqBy(cacheSelectItem.concat(dropdownItems), transKeys(fieldNames, 'id')),
+ transKeys(fieldNames, 'id')
)
-
- const searchable = this.getSearchable()
- this.debouncedFilterItems = debounce(this.onFilterItems.bind(this), 300)
- this.clickOutsideHandel = this.clickOutside.bind(this)
-
- this.state = {
- searchable,
- queryLength: 1,
- focusedIndex: 0,
- selectedItems,
- cacheSelectedItems: selectedItems,
- dropdownItems,
- dropdownShow: false,
- fetching: false,
- keyword: '',
- filterText: '',
- searchInput: {
- width: 2
- }
- }
- }
-
- getChildContext () {
- return {
- component: this
- }
- }
-
- componentWillMount () {
- if (this.isRemote() && this.props.autoload) {
- this.remoteSearch()
- }
- }
-
- componentDidMount () {
- window.addEventListener('click', this.clickOutsideHandel)
- this.resetFocusedIndex()
- }
-
- componentWillUnmount () {
- window.removeEventListener('click', this.clickOutsideHandel)
- }
-
- clickOutside (e) {
- const selectInput = ReactDOM.findDOMNode(this.selectInput)
- if ((selectInput && selectInput.contains(e.target)) || (e.target.tagName === 'INPUT' && e.target.className.includes('hi-select__dropdown__searchbar--input'))) {
- return
- }
- this.hideDropdown()
- }
-
- componentWillReceiveProps (nextProps) {
- if (!_.isEqual(nextProps.data, this.props.data)) {
- const selectedItems = this.resetSelectedItems(
- nextProps.value || this.state.selectedItems,
- nextProps.data,
- []
+ // 在异步多选的时候时候才需要进行值的记录
+ dataSource && type === 'multiple' && setCacheSelectItem(selectedItems)
+ }, [])
+ useEffect(() => {
+ setSearchable(dataSource ? true : propsSearchable)
+ }, [propsSearchable])
+
+ useEffect(() => {
+ setIsFouces(dropdownShow)
+ }, [dropdownShow])
+
+ useEffect(() => {
+ if (value !== undefined) {
+ // 处理默认值的问题
+ const selectedItems = resetSelectedItems(
+ value,
+ _.uniqBy(cacheSelectItem.concat(dropdownItems), transKeys(fieldNames, 'id')),
+ transKeys(fieldNames, 'id')
)
- this.setState({
- selectedItems,
- cacheSelectedItems: selectedItems,
- dropdownItems: cloneDeep(nextProps.data)
- })
- } else {
- if (!_.isEqual(nextProps.value, this.props.value)) {
- const selectedItems = this.resetSelectedItems(
- nextProps.value,
- this.state.dropdownItems,
- []
- ) // 异步获取时会从内部改变dropdownItems,所以不能从list取
- this.setState({
- selectedItems,
- cacheSelectedItems: selectedItems
- })
- }
+ setSelectedItems(selectedItems)
}
- }
-
- getSearchable () {
- const { searchable } = this.props
-
- if (this.isRemote()) {
- return true
- }
- return searchable
- }
-
- parseValue (value = this.props.value) {
- if (Array.isArray(value)) {
- return value.map(v => {
- if (typeof v === 'object') {
- return v.id
- } else {
- return v
- }
- })
- } else {
- return [value]
- }
- }
-
- isRemote () {
- const { dataSource, onSearch } = this.props
- return onSearch || dataSource
- }
-
- resetSelectedItems (value, dropdownItems = [], reviceSelectedItems = []) {
- const values = this.parseValue(value)
- let selectedItems = []
- dropdownItems.forEach(item => {
- if (values.includes(item.id)) {
- selectedItems.push(item)
+ }, [value])
+
+ useEffect(() => {
+ const _data = _.cloneDeep(data)
+ const selectedItems = resetSelectedItems(
+ value === undefined ? defaultValue : value,
+ _data,
+ transKeys(fieldNames, 'id')
+ )
+ setSelectedItems(selectedItems)
+ setDropdownItems(_data)
+ dataSource && type === 'multiple' && setCacheSelectItem(selectedItems)
+ }, [data])
+
+ const localeDatasProps = useCallback(
+ (key) => {
+ if (props[key]) {
+ return props[key]
+ } else {
+ return localeDatas.select[key]
}
- })
- return _.uniqBy(reviceSelectedItems.concat(selectedItems), 'id')
- }
-
- onEnterSelect () {
- const { dropdownItems, focusedIndex } = this.state
- const item = dropdownItems[focusedIndex]
- this.onClickOption(item, focusedIndex)
- }
-
- onChange (selectedItems, changedItems, callback, cacheSelectedItems) {
- const { onChange, value } = this.props
- value === undefined &&
- this.setState(
- {
- selectedItems
- },
- callback
- )
- const selectedIds = selectedItems.map(({ id }) => id)
- onChange && onChange(selectedIds, changedItems)
- }
-
- checkAll (filterItems, e) {
- // 全选
- e && e.stopPropagation()
-
- const { selectedItems } = this.state
- let _selectedItems = [...selectedItems]
- let changedItems = []
- filterItems.forEach(item => {
- if (!item.disabled && this.matchFilter(item)) {
- if (
- !_selectedItems.map(selectItem => selectItem.id).includes(item.id)
- ) {
+ },
+ [props]
+ )
+ // 改变的回调
+ const onChange = useCallback(
+ (selectedItems, changedItems, callback) => {
+ if (value === undefined) {
+ setSelectedItems(selectedItems)
+ callback()
+ }
+ // 调用用户的select
+ const selectedIds = selectedItems.map((item) => item[transKeys(fieldNames, 'id')])
+ propsonChange && propsonChange(selectedIds, changedItems)
+ },
+ [propsonChange]
+ )
+ // 选中某一项
+ const onClickOption = useCallback(
+ (item, index) => {
+ if (!item || item[transKeys(fieldNames, 'disabled')]) return
+
+ let _selectedItems = _.cloneDeep(selectedItems)
+ if (type === 'multiple') {
+ // 获取元素索引
+ const itemIndex = _selectedItems.findIndex((sItem) => {
+ return sItem[transKeys(fieldNames, 'id')] === item[transKeys(fieldNames, 'id')]
+ })
+ if (itemIndex === -1) {
_selectedItems.push(item)
- changedItems.push(item)
+ } else {
+ _selectedItems.splice(itemIndex, 1)
}
- }
- })
- this.onChange(_selectedItems, changedItems, () => {}, _selectedItems)
- }
-
- onClickOption (item, index) {
- if (!item || item.disabled) return
-
- let selectedItems = this.state.selectedItems.concat()
- let cacheSelectedItems = this.state.selectedItems.concat()
- let focusedIndex = index
-
- if (this.props.type === 'multiple') {
- let itemIndex = this.state.selectedItems.findIndex(sItem => {
- return sItem.id === item.id
- })
- if (itemIndex === -1) {
- selectedItems.push(item)
- if (!cacheSelectedItems.map(cacheItem => cacheItem.id).includes(item.id)) {
- cacheSelectedItems.push(item)
+ // 在受控情况下
+ if (_.isEqual(cacheSelectItem, selectedItems) && dataSource) {
+ setCacheSelectItem(_selectedItems)
}
} else {
- selectedItems.splice(itemIndex, 1)
+ _selectedItems = [item]
}
- } else {
- selectedItems = [item]
- this.setState({
- cacheSelectedItems: [item]
- })
- }
-
- this.onChange(selectedItems, item, () => {
- this.setState({
- focusedIndex,
- cacheSelectedItems: this.props.type === 'multiple' ? cacheSelectedItems : [item]
+ onChange(_selectedItems, item, () => {
+ setFocusedIndex(index)
})
- }, this.props.type === 'multiple' ? cacheSelectedItems : [item])
- if (this.props.type !== 'multiple') {
- this.hideDropdown()
- }
- }
+ type !== 'multiple' && hideDropdown()
+ },
+ [type, selectedItems, onChange, dropdownShow, cacheSelectItem]
+ )
- clearKeyword () {
- this.setState(
- {
- keyword: ''
- },
- () => {
- this.selectInput.clearInput()
- }
- )
- }
-
- handleInputClick = e => {
- let { dropdownShow } = this.state
+ // 收起下拉框
+ const hideDropdown = useCallback(() => {
if (dropdownShow) {
- this.hideDropdown()
- return
+ setKeyword('')
+ setDropdownShow(false)
}
-
- !this.getSearchable() && this.selectInput.focus()
- if (this.props.disabled) {
- return
+ // 多选具有默认值的话打开的话应该显示选中的值
+ if (dataSource && type === 'multiple') {
+ setCacheSelectItem(selectedItems)
+ setDropdownItems(selectedItems)
}
-
- if (dropdownShow === false) {
- this.showDropdown()
- }
- }
-
- hideDropdown () {
- this.state.dropdownShow === true &&
- this.setState({ dropdownShow: false, cacheSelectedItems: this.state.selectedItems }, () => {
- this.clearKeyword()
- })
- }
-
- showDropdown () {
- this.state.dropdownShow === false && this.setState({ dropdownShow: true })
- }
-
- deleteItem (item) {
- if (item.disabled) return
- let selectedItems = this.state.selectedItems.concat()
- const sIndex = selectedItems.findIndex((obj, index, arr) => {
- return obj.id === item.id
- })
-
- selectedItems.splice(sIndex, 1)
- this.onChange(selectedItems, item, () => {
- !this.getSearchable() && this.selectInput.focus()
- }, selectedItems)
- }
- // 全部删除
- deleteAllItems () {
- const { type } = this.props
- const focusedIndex = this.resetFocusedIndex()
- const changedItems = [...this.state.selectedItems]
- this.onChange(
- [],
- type === 'multiple' ? changedItems : changedItems[0],
- () => {
- this.setState({ focusedIndex })
- this.onFilterItems('')
- },
- []
- )
- }
-
- remoteSearch (keyword) {
- const { onSearch, dataSource, autoload, withCredentials: propsWithCredentials } = this.props
- if (onSearch && typeof onSearch === 'function') {
- this.setState({
- fetching: true
- })
- onSearch(keyword).finally(() => {
- this.setState({ fetching: false })
- })
- } else {
- const _dataSource = typeof dataSource === 'function' ? dataSource(keyword) : dataSource
- let {
- url,
- transformResponse,
- error,
- params,
- headers,
- mode,
- data = {},
- type = 'GET',
- key,
- withCredentials = propsWithCredentials,
- jsonpCallback = 'callback',
- ...options
- } = _dataSource
-
- keyword =
- !keyword && this.autoloadFlag && autoload
- ? _dataSource.keyword
- : keyword
- this.autoloadFlag = false // 第一次自动加载数据后,输入的关键词即使为空也不再使用默认关键词
- Object.assign(options, {mode}, {headers})
-
- const queryParams = qs.stringify(
- Object.assign({}, params, key && { [key]: keyword })
- )
- if (!_.isEmpty(queryParams)) {
- url = url.includes('?') ? `${url}&${queryParams}` : `${url}?${queryParams}`
- }
- if (type.toUpperCase() === 'POST') {
- options.body = JSON.stringify(data)
- }
-
- this.setState({
- fetching: true
- })
-
- if (type.toUpperCase() === 'JSONP') {
- const _o = {
- jsonpCallback: jsonpCallback,
- jsonpCallbackFunction: jsonpCallback
- }
- fetchJsonp(url, _o)
- .then(res => res.json())
- .then(json => {
- this._setDropdownItems(json, transformResponse)
+ }, [dropdownShow, selectedItems, dataSource, type])
+ // 方向键的回调
+ const moveFocusedIndex = useCallback(
+ (direction) => {
+ let _focusedIndex = focusedIndex
+ if (direction === 'up') {
+ dropdownItems
+ .slice(0, _focusedIndex)
+ .reverse()
+ .every((item) => {
+ _focusedIndex--
+ if (!item[transKeys(fieldNames, 'disabled')] && matchFilter(item)) {
+ return false
+ }
+ return true
})
} else {
- /* eslint-disable */
- fetch(url, {
- method: type,
- credentials: withCredentials ? 'include' : 'same-origin',
- ...options
+ dropdownItems.slice(_focusedIndex + 1).every((item) => {
+ _focusedIndex++
+ if (!item[transKeys(fieldNames, 'disabled')] && matchFilter(item)) {
+ return false
+ }
+ return true
})
- .then(response => response.json())
- .then(
- res => {
- this._setDropdownItems(res, transformResponse)
- },
- err => {
- error && error(err)
- this.setState({
- fetching: false
- })
- }
- )
}
- }
- }
- _setDropdownItems(res, func) {
- let dropdownItems = []
- if (func) {
- dropdownItems = func(res)
- } else {
- dropdownItems = res.data
- }
- if (Array.isArray(dropdownItems)) {
- const reviceSelectedItems = this.props.type === 'multiple' ? this.props.dataSource && this.state.selectedItems || [] : this.state.cacheSelectedItems
- const selectedItems = this.resetSelectedItems(
- this.props.value,
- dropdownItems,
- reviceSelectedItems
- )
- this.setState({
- dropdownItems,
- selectedItems
- })
- }
- this.setState({
- fetching: false
- })
- }
- onFilterItems(keyword) {
- const { onSearch, dataSource, autoload } = this.props
- this.setState(
- {
- keyword: keyword
- },
- () => this.resetFocusedIndex()
- )
- if (dataSource) {
- if (autoload ||(keyword && keyword.length >= this.state.queryLength)) {
- this.remoteSearch(keyword)
+ setFocusedIndex(_focusedIndex)
+ },
+ [focusedIndex, dropdownItems, fieldNames]
+ )
+ // 点击回车选中
+ const onEnterSelect = useCallback(() => {
+ const item = dropdownItems[focusedIndex]
+ onClickOption(item, focusedIndex)
+ }, [dropdownItems, focusedIndex, onClickOption])
+ // 按键操作
+ const handleKeyDown = useCallback(
+ (evt) => {
+ if (evt.keyCode === 13) {
+ onEnterSelect()
}
- } else if(onSearch) {
- this.remoteSearch(keyword)
- }
- }
- matchFilter(item) {
- const { filterOption } = this.props
- const { searchable, keyword } = this.state
+ if (evt.keyCode === 38) {
+ evt.preventDefault()
+ moveFocusedIndex('up')
+ }
+ if (evt.keyCode === 40) {
+ evt.preventDefault()
+ moveFocusedIndex('down')
+ }
+ },
+ [onEnterSelect, moveFocusedIndex, moveFocusedIndex]
+ )
+ // 对关键字的校验 对数据的过滤
+ const matchFilter = useCallback(
+ (item) => {
+ const shouldMatch = dataSource || !searchable || !keyword
+
+ if (typeof filterOption === 'function') {
+ return shouldMatch || filterOption(keyword, item)
+ }
- const shouldMatch = this.isRemote() || !searchable || !keyword
+ return (
+ shouldMatch ||
+ String(item[transKeys(fieldNames, 'id')] || '').includes(keyword) ||
+ String(item[transKeys(fieldNames, 'title')] || '').includes(keyword)
+ )
+ },
+ [dataSource, searchable, keyword, filterOption]
+ )
- if (typeof filterOption === "function") {
- return shouldMatch || filterOption(keyword, item)
+ useEffect(() => {
+ if (dataSource && autoload) {
+ remoteSearch()
}
+ })
- return (
- shouldMatch ||
- String(item.id).includes(keyword) || String(item.title).includes(keyword)
+ const remoteSearch = useCallback(
+ (keyword) => {
+ const _dataSource = typeof dataSource === 'function' ? dataSource(keyword) : dataSource
+ if (Array.isArray(_dataSource)) {
+ setDropdownItems(_dataSource)
+ return
+ }
+ // 处理promise函数
+ if (_dataSource.toString() === '[object Promise]') {
+ setLoading(true)
+ _dataSource.then(
+ (res) => {
+ setLoading(false)
+ setDropdownItems(Array.isArray(res) ? res : [])
+ },
+ () => {
+ setLoading(false)
+ setDropdownItems([])
+ }
+ )
+ return
+ }
+ // 调用接口
+ HiRequestSearch(_dataSource, keyword)
+ },
+ [dataSource, keyword]
+ )
+ const HiRequestSearch = useCallback((_dataSource, keyword) => {
+ const {
+ url,
+ method = 'GET',
+ transformResponse,
+ headers,
+ data = {},
+ params = {},
+ key,
+ error,
+ credentials,
+ withCredentials = false,
+ ...options
+ } = _dataSource
+ // 处理Key
+
+ options.params = key ? { [key]: keyword, ...params } : params
+
+ const _withCredentials = withCredentials || credentials === 'include'
+
+ HiRequest({
+ url,
+ method,
+ data: data,
+ withCredentials: _withCredentials,
+ error,
+ beforeRequest: (config) => {
+ setLoading(true)
+ return config
+ },
+ errorCallback: (err) => {
+ setLoading(false)
+ error && error(err)
+ },
+ ...options
+ }).then(
+ (response) => {
+ setLoading(false)
+ const dataItems = transformResponse && transformResponse(response.data, response)
+ if (Array.isArray(dataItems)) {
+ setDropdownItems(dataItems)
+ } else {
+ console.error('transformResponse return data is not array')
+ }
+ },
+ (error) => {
+ throw error
+ }
)
+ }, [])
+ useEffect(() => {
+ resetFocusedIndex()
+ }, [keyword])
+ // 过滤筛选项
+ const onFilterItems = (keyword) => {
+ setKeyword(keyword)
+
+ if (dataSource && (autoload || keyword)) {
+ remoteSearch(keyword)
+ }
}
-
- resetFocusedIndex(setState = true) {
- let focusedIndex = -1
-
- this.state.dropdownItems.every(item => {
- focusedIndex++
- if (!item.disabled && this.matchFilter(item)) {
+ // 重置下标
+ const resetFocusedIndex = () => {
+ let _focusedIndex = -1
+ dropdownItems.every((item) => {
+ _focusedIndex++
+ if (!item[transKeys(fieldNames, 'disabled')] && matchFilter(item)) {
return false
}
return true
})
- setState &&
- this.setState({
- focusedIndex
- })
- return focusedIndex
+ setFocusedIndex(_focusedIndex)
+ return _focusedIndex
}
- setFocusedIndex(focusedIndex) {
- this.setState({ focusedIndex })
+ // 全部删除
+ const deleteAllItems = () => {
+ onChange([], type === 'multiple' ? selectedItems : selectedItems[0], () => {
+ onFilterItems('')
+ resetFocusedIndex()
+ })
+ setCacheSelectItem([])
}
-
- moveFocusedIndex(direction) {
- let { focusedIndex } = this.state
- const { dropdownItems } = this.state
-
- if (direction === "up") {
- dropdownItems
- .slice(0, focusedIndex)
- .reverse()
- .every(item => {
- focusedIndex--
- if (!item.disabled && this.matchFilter(item)) {
- return false
- }
- return true
- })
- } else {
- dropdownItems.slice(focusedIndex + 1).every(item => {
- focusedIndex++
- if (!item.disabled && this.matchFilter(item)) {
- return false
- }
- return true
- })
+ // 防抖
+ const debouncedFilterItems = _.debounce(onFilterItems, 300)
+ // 全选
+ const checkAll = (e, filterItems, isCheck) => {
+ // 全选
+ e && e.stopPropagation()
+ if (!isCheck) {
+ onChange([], [], () => {})
+ return
}
- this.setState({
- focusedIndex
+ const _selectedItems = [...selectedItems]
+ const changedItems = []
+ filterItems.forEach((item) => {
+ if (!item[transKeys(fieldNames, 'disabled')] && matchFilter(item)) {
+ if (
+ !_selectedItems
+ .map((selectItem) => selectItem[transKeys(fieldNames, 'id')])
+ .includes(item[transKeys(fieldNames, 'id')])
+ ) {
+ _selectedItems.push(item)
+ changedItems.push(item)
+ }
+ }
})
+ onChange(_selectedItems, changedItems, () => {})
}
-
- localeDatasProps(key) {
- const { localeDatas } = this.props
- if (this.props[key]) {
- return this.props[key]
- } else {
- return localeDatas.select[key]
+ // input点击事件
+ const handleInputClick = () => {
+ if (dropdownShow) {
+ hideDropdown()
+ return
}
- }
-
- render() {
- const {
- type,
- showCheckAll,
- className,
- disabled,
- clearable,
- style,
- children,
- optionWidth,
- render,
- multipleWrap,
- onClick,
- onBlur,
- onFocus,
- dataSource,
- filterOption,
- onSearch,
- theme,
- localeDatas
- } = this.props
- const placeholder = this.localeDatasProps('placeholder')
- const emptyContent = this.localeDatasProps('emptyContent')
- const searchPlaceholder = this.localeDatasProps('searchPlaceholder')
- const {
- selectedItems,
- cacheSelectedItems,
- dropdownItems,
- searchable,
- dropdownShow,
- focusedIndex,
- fetching,
- } = this.state
- const extraClass = {
- "is-multiple": type === "multiple",
- "is-single": type === "single"
+ if (disabled) {
+ return
}
- const selectInputWidth = this.selectInputContainer ? this.selectInputContainer.getBoundingClientRect().width : null
- return (
-
-
{
- this.selectInputContainer = node
+ !dropdownShow && setDropdownShow(true)
+ }
+ const placeholder = localeDatasProps('placeholder')
+ const emptyContent = localeDatasProps('emptyContent')
+ const searchPlaceholder = localeDatasProps('searchPlaceholder')
+ const extraClass = {
+ 'is-multiple': type === 'multiple',
+ 'is-single': type === 'single'
+ }
+ const selectInputWidth = selectInputContainer.current
+ ? selectInputContainer.current.getBoundingClientRect().width
+ : null
+ return (
+
+
+ {
+ handleInputClick()
}}
- >
- {
- this.selectInput = node
- }}
- theme={theme}
- mode={type}
- disabled={disabled}
- searchable={searchable}
- clearable={clearable}
- show={dropdownShow && this.props.open}
- dropdownShow={dropdownShow}
- placeholder={placeholder}
- selectedItems={selectedItems}
- dropdownItems={dropdownItems}
- multipleMode={multipleWrap}
- container={this.selectInputContainer}
- moveFocusedIndex={this.moveFocusedIndex.bind(this)}
- onClick={() => {
- if (this.props.open) {
- this.handleInputClick()
- }
- onClick()
- }}
- onBlur={onBlur}
- onFocus={onFocus}
- onDelete={this.deleteItem.bind(this)}
- onClear={this.deleteAllItems.bind(this)}
- onSearch={this.debouncedFilterItems.bind(this)}
- onEnterSelect={this.onEnterSelect.bind(this)}
- />
-
- {children}
-
- { dropdownShow && this.props.open &&
- }
-
+ />
- )
- }
-}
-Select.childContextTypes = {
- component: PropTypes.any
+ {children}
+
{
+ hideDropdown()
+ }}
+ >
+
+
+
+ )
}
+InternalSelect.defaultProps = {
+ data: [],
+ type: 'single',
+ fieldNames: {
+ title: 'title',
+ id: 'id',
+ disabled: 'disabled',
+ children: 'children'
+ },
+ multipleWrap: 'nowrap',
+ disabled: false,
+ clearable: true,
+ defaultValue: '',
+ autoload: false,
+ showCheckAll: false,
+ showJustSelected: false,
+ open: true,
+ onClick: () => {},
+ onBlur: () => {},
+ onFocus: () => {}
+}
+const Select = forwardRef((props, ref) => {
+ return
+})
export default Provider(Select)
diff --git a/components/select/SelectDropdown.js b/components/select/SelectDropdown.js
index 764909091..10d205c18 100644
--- a/components/select/SelectDropdown.js
+++ b/components/select/SelectDropdown.js
@@ -1,261 +1,312 @@
-import React, { Component } from 'react'
+import React, { useState, useEffect, useRef, useCallback } from 'react'
import classNames from 'classnames'
import Checkbox from '../checkbox'
import Loading from '../loading'
import Icon from '../icon'
+import { transKeys } from './utils'
-class SelectDropdown extends Component {
- constructor (props) {
- super(props)
- this.state = {
- filterItems: this.props.dropdownItems,
- searchbarValue: '',
- cachedropdownItems: this.props.dropdownItems
- }
- }
- static getDerivedStateFromProps (nextProps, prevState) {
- const {selectedItems, mode, isOnSearch, dropdownItems, show} = nextProps
- const {searchbarValue, cachedropdownItems} = prevState
- const _filterItems = selectedItems.length > 0 && searchbarValue.length === 0 && mode === 'single' && isOnSearch ? cachedropdownItems : dropdownItems
- const _searchbarValue = show ? searchbarValue : ''
- return {filterItems: _filterItems, searchbarValue: _searchbarValue}
- }
- componentDidMount () {
- this.focus()
- }
- focus = () => {
- this.props.searchable && setTimeout(() => this.searchbar && this.searchbar.focus(), 0)
- }
- onClickOption (e, item, index) {
- e.stopPropagation()
- e.preventDefault()
- if (item.disabled) {
- return
- }
- this.props.mode === 'single' && this.props.isOnSearch && this.setState({
- cachedropdownItems: this.props.dropdownItems
- })
- this.props.onClickOption(item, index)
- }
- filterOptions = (keyword) => {
- const { dropdownItems, filterOption } = this.props
- let filterItems = []
- if (typeof filterOption === 'function' || keyword === '') {
- filterItems = dropdownItems
- } else {
- dropdownItems.map((item) => {
- String(item.title).includes(keyword) && filterItems.push(item)
- })
- }
- this.setState({
- filterItems: filterItems,
- searchbarValue: keyword
- })
- }
- searchEvent (e) {
- const filterText = e.target.value
- this.filterOptions(filterText)
- this.props.onSearch(filterText)
-
- this.setState({
- searchbarValue: filterText
- })
- }
- hightlightKeyword (text, uniqueKey) {
- const { searchbarValue } = this.state
- let _keyword = this.state.searchbarValue
- _keyword = searchbarValue.includes('[') ? _keyword.replace(/\[/gi, '\\[') : _keyword
- _keyword = searchbarValue.includes('(') ? _keyword.replace(/\(/gi, '\\(') : _keyword
- _keyword = searchbarValue.includes(')') ? _keyword.replace(/\)/gi, '\\)') : _keyword
+const SelectDropdown = ({
+ mode,
+ matchFilter,
+ emptyContent,
+ loading,
+ optionWidth,
+ showCheckAll,
+ showJustSelected,
+ dropdownRender,
+ theme,
+ searchable,
+ onFocus,
+ onBlur,
+ searchPlaceholder,
+ dropdownItems,
+ localeMap,
+ handleKeyDown,
+ onSearch,
+ isOnSearch,
+ onClickOption,
+ checkAll,
+ selectInputWidth,
+ selectedItems,
+ show,
+ fieldNames
+}) => {
+ const [filterItems, setFilterItems] = useState(dropdownItems)
+ const [searchbarValue, setSearchbarValue] = useState('')
+ const [ischeckAll, setIscheckAll] = useState(false)
+ const searchbar = useRef('')
+ useEffect(() => {
+ setFilterItems(dropdownItems)
+ }, [dropdownItems])
- let parts = text.toString().split(new RegExp(`(${_keyword})`, 'gi'))
- return (
- this.state.searchbarValue.length > 0 ?
- { parts.map((part, i) =>
- part === searchbarValue
- ?
- { part }
-
- : part
+ // 监控全选功能
+ useEffect(() => {
+ setIscheckAll(selectedItems.length > 0 && selectedItems.length === filterItems.length)
+ }, [selectedItems, filterItems])
+ // 让搜索框获取焦点
+ useEffect(() => {
+ searchable && setTimeout(() => searchbar.current && searchbar.current.focus(), 0)
+ }, [])
+ // 仅看已选
+ const showSelected = useCallback(
+ (check) => {
+ if (check) {
+ const values = selectedItems.map((item) => {
+ return item[transKeys(fieldNames, 'id')]
+ })
+ setFilterItems(
+ dropdownItems.filter((item) => {
+ return values.includes(item[transKeys(fieldNames, 'id')])
+ })
)
- }
-
- : text
- )
- }
- onMouseEnter (item, index) {
- !item.disabled && this.props.setFocusedIndex(index)
- }
-
- itemSelected (item) {
- const selectedItems = this.props.selectedItems
+ } else {
+ setFilterItems(dropdownItems)
+ }
+ },
+ [selectedItems, fieldNames, dropdownItems]
+ )
+ useEffect(() => {
+ const _filterItems = dropdownItems
+ setFilterItems(_filterItems)
+ }, [mode, isOnSearch, dropdownItems, show])
- return selectedItems.map((item) => item.id).indexOf(item.id) > -1
+ let matched = 0
+ const style = optionWidth && {
+ width: optionWidth
}
- cleanSearchbarValue (e) {
- e.stopPropagation()
- const filterText = ''
- this.filterOptions(filterText)
- this.props.onSearch(filterText)
- this.setState({
- searchbarValue: filterText
- })
- }
- handleKeyDown (evt) {
- if (evt.keyCode === 13) {
- this.props.onEnterSelect()
- }
+ const filterOptions = useCallback(
+ (keyword) => {
+ setFilterItems(dropdownItems)
+ setSearchbarValue(keyword)
+ },
+ [dropdownItems]
+ )
+ const searchEvent = useCallback(
+ (e) => {
+ const filterText = e.target.value
+ filterOptions(filterText)
+ onSearch(filterText)
+ },
+ [onSearch]
+ )
- if (evt.keyCode === 38) {
- evt.preventDefault()
- this.props.moveFocusedIndex('up')
- }
- if (evt.keyCode === 40) {
- evt.preventDefault()
- this.props.moveFocusedIndex('down')
- }
- }
- renderOption (mode, isSelected, item) {
- if (item.children) {
- return item.children
- }
- if (this.props.dropdownRender) {
- return this.props.dropdownRender(item, isSelected)
+ const cleanSearchbarValue = useCallback(
+ (e) => {
+ e.stopPropagation()
+ const filterText = ''
+ filterOptions(filterText)
+ onSearch(filterText)
+ },
+ [onSearch]
+ )
+ // 是否被选中
+ const itemSelected = useCallback(
+ (item) => {
+ return (
+ selectedItems.map((item) => item[transKeys(fieldNames, 'id')]).indexOf(item[transKeys(fieldNames, 'id')]) > -1
+ )
+ },
+ [selectedItems, fieldNames]
+ )
+ // 点击某个选项时
+ const onClickOptionIntal = useCallback(
+ (e, item, index) => {
+ e.stopPropagation()
+ e.preventDefault()
+ if (item[transKeys(fieldNames, 'disabled')]) {
+ return
+ }
+ onClickOption(item, index)
+ },
+ [onClickOption, fieldNames]
+ )
+ // 高亮关键字
+ const hightlightKeyword = useCallback(
+ (text = '', uniqueKey) => {
+ let _keyword = searchbarValue
+ _keyword = searchbarValue.includes('[') ? _keyword.replace(/\[/gi, '\\[') : _keyword
+ _keyword = searchbarValue.includes('(') ? _keyword.replace(/\(/gi, '\\(') : _keyword
+ _keyword = searchbarValue.includes(')') ? _keyword.replace(/\)/gi, '\\)') : _keyword
+
+ const parts = text.split(new RegExp(`(${_keyword})`, 'gi'))
+ return searchbarValue.length > 0 ? (
+
+ {parts.map((part, i) =>
+ part === searchbarValue ? (
+
+ {part}
+
+ ) : (
+ part
+ )
+ )}
+
+ ) : (
+ text
+ )
+ },
+ [searchbarValue]
+ )
+ // 渲染单个选项
+ const renderOption = (isSelected, item) => {
+ if (dropdownRender) {
+ return dropdownRender(item, isSelected)
}
const paddingNum = mode === 'multiple' ? 48 : 24
+ // 提高可读性
+ let width = selectInputWidth ? selectInputWidth - paddingNum : null
+ if (optionWidth) {
+ width = optionWidth - paddingNum
+ }
const style = {
- width: this.props.optionWidth ? this.props.optionWidth - paddingNum : this.props.selectInputWidth ? this.props.selectInputWidth - paddingNum : null
+ width
}
return (
{mode === 'multiple' && (
- {
- this.props.isOnSearch ? item.title : this.hightlightKeyword(item.title, item.id)
- }
+
+ {isOnSearch
+ ? item[transKeys(fieldNames, 'title')]
+ : hightlightKeyword(item[transKeys(fieldNames, 'title')], item[transKeys(fieldNames, 'id')])}
+
)}
{mode === 'single' && (
- {
- this.props.isOnSearch ? item.title : this.hightlightKeyword(item.title, item.id)
- }
- )}
- {mode === 'single' && isSelected && (
-
-
+
+ {isOnSearch
+ ? item[transKeys(fieldNames, 'title')]
+ : hightlightKeyword(item[transKeys(fieldNames, 'title')], item[transKeys(fieldNames, 'id')])}
)}
)
}
-
- render () {
- const {
- mode,
- focusedIndex,
- matchFilter,
- noFoundTip,
- loading,
- optionWidth,
- showCheckAll,
- dropdownRender,
- theme,
- searchable,
- onFocus,
- onBlur,
- searchPlaceholder,
- localeMap
- } = this.props
- const { filterItems, searchbarValue } = this.state
- let matched = 0
- const style = optionWidth && {
- width: optionWidth
- }
-
+ const groupItem = (filterGroupItem, filterItemsIndex) => {
+ const renderGroup = []
+ const label = (
+
-
+ {filterGroupItem[transKeys(fieldNames, 'title')]}
+
+ )
+ renderGroup.push(label)
+ filterGroupItem[transKeys(fieldNames, 'children')].forEach((item, index) => {
+ renderGroup.push(normalItem(item, filterItemsIndex + 1 + '-' + index))
+ })
+ return renderGroup
+ }
+ const normalItem = (item, filterItemsIndex) => {
+ matched++
+ const isSelected = itemSelected(item)
+ const isDisabled = item[transKeys(fieldNames, 'disabled')]
return (
-
onClickOptionIntal(e, item, filterItemsIndex)}
+ key={item[transKeys(fieldNames, 'id')]}
+ index={filterItemsIndex}
>
- {searchable &&
-
-
-
+ {renderOption(isSelected, item, filterItemsIndex)}
+
+ )
+ }
+ const renderItems = () => {
+ return (
+
+ {filterItems &&
+ filterItems.map((item, filterItemsIndex) => {
+ if (matchFilter(item)) {
+ return item[transKeys(fieldNames, 'children')]
+ ? groupItem(item, filterItemsIndex)
+ : normalItem(item, filterItemsIndex)
+ }
+ })}
+ {matched === 0 && (
+ - e.stopPropagation()}
+ >
+ {emptyContent}
+
+ )}
+
+ )
+ }
+ return (
+
+ {searchable && (
+
+
+
+
+
{
- this.searchbar = input
- }}
+ clearable="true"
+ ref={searchbar}
value={searchbarValue}
- onFocus={onFocus.bind(this)}
- onBlur={onBlur.bind(this)}
- clearabletrigger='always'
- onKeyDown={this.handleKeyDown.bind(this)}
- onInput={this.searchEvent.bind(this)}
- onChange={this.searchEvent.bind(this)}
+ onFocus={onFocus}
+ onBlur={onBlur}
+ clearabletrigger="always"
+ onKeyDown={handleKeyDown}
+ onChange={searchEvent}
/>
- {searchbarValue.length > 0 ? : null}
-
-
}
- {loading && (
-
-
+ {searchbarValue.length > 0 ? (
+
+
+
+ ) : null}
- )}
- {!loading && (
-
+ )}
+ {loading && (
+
+
+
+ )}
- {
- filterItems.map((item, index) => {
- if (matchFilter(item)) {
- matched++
- const isSelected = this.itemSelected(item)
- const isDisabled = item.disabled
- return (
-
- this.onClickOption(e, item, index)}
- key={item.id}
- data-focused={focusedIndex === index}
- onMouseEnter={() => this.onMouseEnter(item, index)}
- >
- {this.renderOption(mode, isSelected, item)}
-
- )
- }
- })
- }
- {matched === 0 && (
-
- e.stopPropagation()}
+ {!loading && renderItems()}
+
+ {mode === 'multiple' && (showCheckAll || showJustSelected) && (
+
+
+ {showCheckAll && (
+
{
+ checkAll(e, filterItems, e.target.checked)
+ }}
>
- {noFoundTip}
-
+ {localeMap.checkAll}
+
)}
-
- )}
- {mode === 'multiple' && showCheckAll && (
-
- {localeMap['checkAll']}
- )}
-
- )
- }
+
+ {showJustSelected && (
+ {
+ showSelected(e.target.checked)
+ }}
+ >
+ {localeMap.justSelected}
+
+ )}
+
+
+ )}
+
+ )
}
export default SelectDropdown
diff --git a/components/select/SelectInput.js b/components/select/SelectInput.js
index 1b53acae3..9b0bf2f0e 100644
--- a/components/select/SelectInput.js
+++ b/components/select/SelectInput.js
@@ -1,293 +1,17 @@
-import React, { Component } from 'react'
-import classNames from 'classnames'
-import { getTextWidth } from './common.js'
-
-class SelectInput extends Component {
- constructor (props) {
- super(props)
-
- this.state = {
- showCount: 0,
- value: '',
- inputStyle: {
- width: 2
- },
- cacheselectedItems: []
- }
- this.wrapperRect = this.itemsRef && this.itemsRef.getBoundingClientRect()
- }
-
- calShowCountFlag = true
- componentDidMount() {
- this.wrapperRect = this.itemsRef && this.itemsRef.getBoundingClientRect()
- }
- componentDidUpdate () {
- if (
- this.props.multipleMode === 'nowrap' &&
- this.calShowCountFlag &&
- this.itemsRef
- ) {
- // 多选超过一行时以数字显示
- const itemsRect = this.wrapperRect
- let width = 0
- let showCount = 0
- const items = this.itemsRef.querySelectorAll('.hi-select__input--item')
-
- for (const item of items) {
- const itemRect = item.getBoundingClientRect()
- width += itemRect.width
- if (
- width + 50 < itemsRect.width ||
- (width > itemsRect.width &&
- width + 50 <= itemsRect.width &&
- showCount + 1 === items.length)
- ) {
- // 50是留给显示剩余选项的空间
- ++showCount
- } else {
- break
- }
- }
- this.setState({ showCount })
- this.calShowCountFlag = false
- } else {
- this.calShowCountFlag = true
- }
- }
-
- static getDerivedStateFromProps (nextProps, nextState) {
- return nextProps.dropdownShow
- ? { cacheselectedItems: nextProps.selectedItems.length > 0 ? nextProps.selectedItems : nextState.cacheselectedItems } : { cacheselectedItems: nextProps.selectedItems }
- }
- focus () {
- this.wrapperRect = this.itemsRef && this.itemsRef.getBoundingClientRect()
- setTimeout(() => this.searchInput && this.searchInput.focus(), 0)
- }
-
- handleKeywordChange (evt) {
- var val = evt.target.value
- this.setState({
- value: val,
- inputStyle: {
- width: getTextWidth(val)
- }
- })
- this.props.onSearch(evt.target.value)
- }
-
- clearInput () {
- this.searchInput && (this.searchInput.value = '')
- this.setState({
- value: ''
- })
- }
-
- handleKeyDown (evt) {
- if (evt.keyCode === 13) {
- this.props.onEnterSelect()
- }
-
- if (evt.keyCode === 38) {
- evt.preventDefault()
- this.props.moveFocusedIndex('up')
- }
- if (evt.keyCode === 40) {
- evt.preventDefault()
- this.props.moveFocusedIndex('down')
- }
- }
-
- handleClear () {
- this.setState({
- cacheselectedItems: []
- })
- this.props.onClear()
- this.clearInput()
- }
-
- renderMultiple () {
- let {
- placeholder,
- selectedItems,
- dropdownShow,
- disabled,
- searchable,
- clearable,
- multipleMode,
- onFocus,
- theme,
- onBlur
- } = this.props
- let icon = dropdownShow ? 'up' : 'down'
- let { showCount, value, inputStyle } = this.state
- showCount =
- showCount === 0 || this.calShowCountFlag
- ? selectedItems.length
- : showCount
-
- if (!selectedItems.length) {
- inputStyle = { width: '100%' }
- }
- return (
-
- {selectedItems.length === 0 && !value && (
-
{placeholder}
- )}
-
{
- this.itemsRef = node
- }}
- >
- {selectedItems.slice(0, showCount).map((item, index) => {
- const _item = (
-
-
{item.title}
-
{
- e.stopPropagation()
- !disabled && this.props.onDelete(item)
- }}
- >
-
-
-
- )
- return _item
- })}
- {showCount < selectedItems.length && (
-
- +
-
- {selectedItems.length - showCount}
-
-
- )}
- {searchable && !disabled && (
-
- {
- this.searchInput = input
- }}
- onChange={this.handleKeywordChange.bind(this)}
- onKeyDown={this.handleKeyDown.bind(this)}
- onFocus={onFocus.bind(this)}
- onBlur={onBlur.bind(this)}
- readOnly
- />
-
- )}
-
-
- 0 }
- )}
- />
- {clearable && selectedItems.length > 0 && (
-
- )}
-
-
- )
- }
-
- renderSingle () {
- let {
- placeholder,
- selectedItems,
- dropdownShow,
- disabled,
- clearable,
- onFocus,
- theme,
- onBlur
- } = this.props
-
- selectedItems = selectedItems.length > 0 ? selectedItems : this.state.cacheselectedItems
- placeholder =
- selectedItems.length > 0 ? selectedItems[0].title : placeholder
- let icon = dropdownShow ? 'up' : 'down'
- return (
-
{
- this.itemsRef = node
- }}
- >
-
0)
- })}
- style={{width: this.wrapperRect && this.wrapperRect.width-46}}
- >
-
- {selectedItems[0] && selectedItems[0].title}
-
-
- {(dropdownShow || selectedItems.length === 0) && (
-
0})} style={{width:this.wrapperRect && this.wrapperRect.width-46}}>
- {
- this.searchInput = input
- }}
- value={selectedItems.length > 0 ? placeholder : ''}
- placeholder={placeholder}
- onChange={this.handleKeywordChange.bind(this)}
- onKeyDown={this.handleKeyDown.bind(this)}
- onFocus={onFocus.bind(this)}
- onBlur={onBlur.bind(this)}
- readOnly
- />
-
- )}
-
- 0 }
- )}
- />
- {clearable && selectedItems.length > 0 && (
-
- )}
-
-
- )
- }
-
- render () {
- let { mode } = this.props
-
- if (mode === 'multiple') {
- return this.renderMultiple()
- } else {
- return this.renderSingle()
- }
- }
+import React, { forwardRef } from 'react'
+import SingleInput from './SingleInput'
+import MultipleInput from './MultipleInput'
+
+const InternalSelectInput = (props) => {
+ const { mode, handleKeyDown } = props
+
+ return mode === 'multiple' ? (
+
+ ) : (
+
+ )
}
-
+const SelectInput = forwardRef((props, ref) => {
+ return
+})
export default SelectInput
diff --git a/components/select/SingleInput.js b/components/select/SingleInput.js
new file mode 100644
index 000000000..0083c4965
--- /dev/null
+++ b/components/select/SingleInput.js
@@ -0,0 +1,93 @@
+import React, { useState, useEffect } from 'react'
+import classNames from 'classnames'
+import { transKeys } from './utils'
+
+// 单选输入框
+const SingleInput = (props) => {
+ let {
+ placeholder,
+ dropdownShow,
+ disabled,
+ clearable,
+ onFocus,
+ theme,
+ onBlur,
+ onClick,
+ selectedItems: propsSelectItem,
+ handleKeyDown,
+ onClear,
+ fieldNames,
+ isFocus
+ } = props
+ const [cacheselectedItems, setCacheselectedItems] = useState(propsSelectItem || [])
+ useEffect(() => {
+ setCacheselectedItems(propsSelectItem)
+ }, [propsSelectItem])
+
+ const handleClear = (e) => {
+ e.stopPropagation()
+
+ onClear()
+ setCacheselectedItems([])
+ }
+
+ const icon = dropdownShow ? 'up' : 'down'
+
+ const selectedItems = propsSelectItem.length > 0 ? propsSelectItem : cacheselectedItems
+
+ placeholder = selectedItems.length > 0 ? selectedItems[0][transKeys(fieldNames, 'title')] : placeholder
+
+ return (
+
+
0)
+ })}
+ >
+
+ {selectedItems[0] && selectedItems[0][transKeys(fieldNames, 'title')]}
+
+
+ {(dropdownShow || selectedItems.length === 0) && (
+
0
+ })}
+ >
+ 0 ? placeholder : ''}
+ placeholder={placeholder}
+ onKeyDown={handleKeyDown}
+ onFocus={onFocus}
+ onBlur={onBlur}
+ readOnly
+ />
+
+ )}
+
+ 0
+ })}
+ />
+ {clearable && selectedItems.length > 0 && (
+
+ )}
+
+
+ )
+}
+
+export default SingleInput
diff --git a/components/select/index.d.ts b/components/select/index.d.ts
new file mode 100644
index 000000000..23dd47a31
--- /dev/null
+++ b/components/select/index.d.ts
@@ -0,0 +1,51 @@
+import { CSSProperties } from "react"
+
+type DataItem = {
+ id: string | number
+ title: string
+ disabled?: boolean
+ children?: DataItem[]
+}
+export type DataSource = {
+ url: string
+ type?: 'get' | 'post'
+ data?: object
+ params?: object
+ headers?: object
+ mode?: 'same-origin' | 'cors' | 'no-cors' | 'navigate'
+ transformResponse?: (response: object) => DataItem[]
+}
+type FieldNames = {
+ id?: string
+ title?: string
+ disabled?: string
+ children?: string
+}
+const DataSourFun: (keyword: string) => DataSource
+const FilterOptionFun: (keyword: string, item: DataItem) => boolean
+interface Props {
+ type?: 'single' | 'multiple'
+ data?: DataItem[]
+ fieldNames?: FieldNames
+ dataSource?: DataSource | DataSourFun
+ value?: string | string[]
+ defaultValue?: string | string[]
+ showCheckAll?: boolean
+ showJustSelected?: boolean
+ multipleWrap?: 'wrap' | 'nowrap'
+ searchable?: boolean
+ filterOption?: FilterOptionFun
+ clearable?: boolean
+ autoload?: boolean
+ disabled?: boolean
+ placeholder?: string
+ emptyContent?: string | JSX.Element
+ style?: CSSProperties
+ optionWidth?: number
+ onChange?: (selectedIds: string[], changedItem: DataItem) => void
+ render?: (item: DataItem, selected: boolean) => JSX.Element
+ overlayClassName?: string
+ setOverlayContainer?: (triggerNode: any) => any
+}
+declare const Select: React.ComponentType
+export default Select
diff --git a/components/select/index.js b/components/select/index.js
index 5243259eb..45b143b9a 100644
--- a/components/select/index.js
+++ b/components/select/index.js
@@ -1,6 +1,14 @@
+import React from 'react'
import Select from './Select'
-import SelectLegacy from './select-legacy'
+import SelectV1 from './select-legacy/select-v1'
+import SelectV2 from './select-legacy/select-v2'
import './style/index'
-import SwitchVersion from '../_util/SwitchVersion'
-export default SwitchVersion(Select, SelectLegacy)
+const SelectWrapper = ({ legacy, legacyV2, ...props }) => {
+ let WrapperComponent = legacy ? SelectV1 : Select
+ WrapperComponent = legacyV2 ? SelectV2 : WrapperComponent
+
+ return
+}
+
+export default SelectWrapper
diff --git a/components/select/select-legacy/Option.js b/components/select/select-legacy/select-v1/Option.js
similarity index 100%
rename from components/select/select-legacy/Option.js
rename to components/select/select-legacy/select-v1/Option.js
diff --git a/components/select/select-legacy/Select.js b/components/select/select-legacy/select-v1/Select.js
similarity index 66%
rename from components/select/select-legacy/Select.js
rename to components/select/select-legacy/select-v1/Select.js
index fcb5abcfd..9eab0ca8e 100644
--- a/components/select/select-legacy/Select.js
+++ b/components/select/select-legacy/select-v1/Select.js
@@ -5,10 +5,10 @@ import PropTypes from 'prop-types'
import shallowEqual from 'shallowequal'
import debounce from 'lodash/debounce'
import cloneDeep from 'lodash/cloneDeep'
-import Popper from '../../popper'
+import Popper from '../../../popper'
import SelectInput from './SelectInput'
import SelectDropdown from './SelectDropdown'
-import Provider from '../../context'
+import Provider from '../../../context'
import fetchJsonp from 'fetch-jsonp'
class Select extends Component {
autoloadFlag = true // 第一次自动加载数据标识
@@ -58,11 +58,12 @@ class Select extends Component {
constructor (props) {
super(props)
- let {
- list
- } = this.props
+ let { list } = this.props
const dropdownItems = cloneDeep(list)
- const selectedItems = this.resetSelectedItems(this.props.value, dropdownItems)
+ const selectedItems = this.resetSelectedItems(
+ this.props.value,
+ dropdownItems
+ )
const searchable = this.getSearchable()
this.debouncedFilterItems = debounce(this.onFilterItems.bind(this), 300)
this.clickOutsideHandel = this.clickOutside.bind(this)
@@ -114,16 +115,26 @@ class Select extends Component {
componentWillReceiveProps (props) {
if (!shallowEqual(props.value, this.props.value)) {
- const selectedItems = this.resetSelectedItems(props.value, this.state.dropdownItems) // 异步获取时会从内部改变dropdownItems,所以不能从list取
-
- this.setState({
- selectedItems
- }, () => {
- // this.onChange()
- })
+ const selectedItems = this.resetSelectedItems(
+ props.value,
+ this.state.dropdownItems
+ ) // 异步获取时会从内部改变dropdownItems,所以不能从list取
+
+ this.setState(
+ {
+ selectedItems
+ },
+ () => {
+ // this.onChange()
+ }
+ )
}
if (!shallowEqual(props.list, this.props.list)) {
- const selectedItems = this.resetSelectedItems(props.value, props.list, true)
+ const selectedItems = this.resetSelectedItems(
+ props.value,
+ props.list,
+ true
+ )
this.setState({
selectedItems,
@@ -133,9 +144,7 @@ class Select extends Component {
}
getSearchable () {
- let {
- searchable
- } = this.props
+ let { searchable } = this.props
if (this.isRemote()) {
return true
@@ -157,25 +166,28 @@ class Select extends Component {
}
isRemote () {
- let {
- origin
- } = this.props
+ let { origin } = this.props
return origin && !!origin.url
}
resetSelectedItems (value, dropdownItems, listChanged = false) {
const values = this.parseValue(value)
- const selectedItems = listChanged && this.props.mode === 'multiple' ? this.state.selectedItems : [] // 如果是多选,dropdownItems有改动,需要保留之前的选中值
-
- dropdownItems && dropdownItems.map(item => {
- if (values.indexOf(item.id) !== -1) {
- let itemIndex = selectedItems.findIndex((sItem) => { // 多选时检查是否已选中
- return sItem.id === item.id
- })
-
- itemIndex === -1 && selectedItems.push(item)
- }
- })
+ const selectedItems =
+ listChanged && this.props.mode === 'multiple'
+ ? this.state.selectedItems
+ : [] // 如果是多选,dropdownItems有改动,需要保留之前的选中值
+
+ dropdownItems &&
+ dropdownItems.map(item => {
+ if (values.indexOf(item.id) !== -1) {
+ let itemIndex = selectedItems.findIndex(sItem => {
+ // 多选时检查是否已选中
+ return sItem.id === item.id
+ })
+
+ itemIndex === -1 && selectedItems.push(item)
+ }
+ })
return selectedItems
}
@@ -188,10 +200,7 @@ class Select extends Component {
}
onEnterSelect () {
- const {
- dropdownItems,
- focusedIndex
- } = this.state
+ const { dropdownItems, focusedIndex } = this.state
const item = dropdownItems[focusedIndex]
this.onClickOption(item, focusedIndex)
@@ -212,18 +221,23 @@ class Select extends Component {
let changedItems = []
dropdownItems.forEach(item => {
if (!item.disabled && this.matchFilter(item)) {
- if (!_selectedItems.map(selectItem => selectItem.id).includes(item.id)) {
+ if (
+ !_selectedItems.map(selectItem => selectItem.id).includes(item.id)
+ ) {
_selectedItems.push(item)
changedItems.push(item)
}
}
})
- this.setState({
- selectedItems: _selectedItems
- }, () => {
- this.selectInput.focus()
- this.onChange(changedItems)
- })
+ this.setState(
+ {
+ selectedItems: _selectedItems
+ },
+ () => {
+ this.selectInput.focus()
+ this.onChange(changedItems)
+ }
+ )
}
onClickOption (item, index) {
@@ -233,7 +247,7 @@ class Select extends Component {
let focusedIndex = index
if (this.props.mode === 'multiple') {
- let itemIndex = this.state.selectedItems.findIndex((sItem) => {
+ let itemIndex = this.state.selectedItems.findIndex(sItem => {
return sItem.id === item.id
})
if (itemIndex === -1) {
@@ -245,33 +259,37 @@ class Select extends Component {
selectedItems = [item]
}
- this.setState({
- selectedItems,
- focusedIndex
- }, () => {
- if (this.props.mode !== 'multiple') {
- this.hideDropdown()
- } else {
- this.selectInput.focus()
- this.clearKeyword() // 多选状态清空筛选
- }
+ this.setState(
+ {
+ selectedItems,
+ focusedIndex
+ },
+ () => {
+ if (this.props.mode !== 'multiple') {
+ this.hideDropdown()
+ } else {
+ this.selectInput.focus()
+ this.clearKeyword() // 多选状态清空筛选
+ }
- this.onChange(item)
- })
+ this.onChange(item)
+ }
+ )
}
clearKeyword () {
- this.setState({
- keyword: ''
- }, () => {
- this.selectInput.clearInput()
- })
+ this.setState(
+ {
+ keyword: ''
+ },
+ () => {
+ this.selectInput.clearInput()
+ }
+ )
}
- handleInputClick = (e) => {
- let {
- dropdownShow
- } = this.state
+ handleInputClick = e => {
+ let { dropdownShow } = this.state
if (dropdownShow) {
this.hideDropdown()
@@ -289,13 +307,14 @@ class Select extends Component {
}
hideDropdown () {
- this.state.dropdownShow === true && this.setState({dropdownShow: false}, () => {
- this.clearKeyword()
- })
+ this.state.dropdownShow === true &&
+ this.setState({ dropdownShow: false }, () => {
+ this.clearKeyword()
+ })
}
showDropdown () {
- this.state.dropdownShow === false && this.setState({dropdownShow: true})
+ this.state.dropdownShow === false && this.setState({ dropdownShow: true })
this.selectInput.focus()
}
@@ -308,24 +327,32 @@ class Select extends Component {
})
selectedItems.splice(sIndex, 1)
- this.setState({
- selectedItems
- }, () => {
- this.selectInput.focus()
- this.onChange(item)
- })
+ this.setState(
+ {
+ selectedItems
+ },
+ () => {
+ this.selectInput.focus()
+ this.onChange(item)
+ }
+ )
}
// 全部删除
deleteAllItems () {
const focusedIndex = this.resetFocusedIndex()
const changedItems = [...this.state.selectedItems]
- this.setState({
- focusedIndex,
- selectedItems: []
- }, () => {
- this.onChange(this.props.mode === 'multiple' ? changedItems : changedItems[0])
- this.onFilterItems('')
- })
+ this.setState(
+ {
+ focusedIndex,
+ selectedItems: []
+ },
+ () => {
+ this.onChange(
+ this.props.mode === 'multiple' ? changedItems : changedItems[0]
+ )
+ this.onFilterItems('')
+ }
+ )
}
remoteSearch (keyword) {
@@ -339,9 +366,15 @@ class Select extends Component {
jsonpCallback = 'callback',
...options
} = this.props.origin
- keyword = !keyword && this.autoloadFlag && this.props.autoload ? this.props.origin.keyword : keyword
+ keyword =
+ !keyword && this.autoloadFlag && this.props.autoload
+ ? this.props.origin.keyword
+ : keyword
this.autoloadFlag = false // 第一次自动加载数据后,输入的关键词即使为空也不再使用默认关键词
- url = url.indexOf('?') === -1 ? `${url}?${[key]}=${keyword}` : `${url}&${[key]}=${keyword}`
+ url =
+ url.indexOf('?') === -1
+ ? `${url}?${[key]}=${keyword}`
+ : `${url}&${[key]}=${keyword}`
if (type.toUpperCase() === 'POST') {
options.body = JSON.stringify(data)
@@ -351,23 +384,33 @@ class Select extends Component {
})
if (type.toUpperCase() === 'JSONP') {
- const _o = {jsonpCallback: jsonpCallback, jsonpCallbackFunction: jsonpCallback}
- fetchJsonp(url, _o).then((res) => res.json()).then((json) => { this._setDropdownItems(json, func) })
+ const _o = {
+ jsonpCallback: jsonpCallback,
+ jsonpCallbackFunction: jsonpCallback
+ }
+ fetchJsonp(url, _o)
+ .then(res => res.json())
+ .then(json => {
+ this._setDropdownItems(json, func)
+ })
} else {
/* eslint-disable */
fetch(url, {
method: type,
...options
})
- .then(response => response.json())
- .then(res => {
- this._setDropdownItems(res, func)
- }, err => {
- error && error(err)
- this.setState({
- fetching: false
- })
- })
+ .then(response => response.json())
+ .then(
+ res => {
+ this._setDropdownItems(res, func)
+ },
+ err => {
+ error && error(err)
+ this.setState({
+ fetching: false
+ })
+ }
+ )
}
}
_setDropdownItems(res, func) {
@@ -378,7 +421,11 @@ class Select extends Component {
dropdownItems = res.data
}
if (Array.isArray(dropdownItems)) {
- const selectedItems = this.resetSelectedItems(this.props.value, dropdownItems, true)
+ const selectedItems = this.resetSelectedItems(
+ this.props.value,
+ dropdownItems,
+ true
+ )
this.setState({
dropdownItems,
selectedItems
@@ -388,30 +435,41 @@ class Select extends Component {
fetching: false
})
}
- onFilterItems (keyword) {
- this.setState({
- keyword
- }, ()=>this.resetFocusedIndex())
+ onFilterItems(keyword) {
+ this.setState(
+ {
+ keyword
+ },
+ () => this.resetFocusedIndex()
+ )
if (this.props.origin) {
// this.setState({
// dropdownItems: []
// })
- if (this.props.autoload || keyword.toString().length >= this.state.queryLength) {
+ if (
+ this.props.autoload ||
+ keyword.toString().length >= this.state.queryLength
+ ) {
this.remoteSearch(keyword)
}
}
}
- matchFilter (item) {
- const {
- searchable,
- keyword
- } = this.state
- return this.isRemote() || (!searchable || !keyword) || (searchable && keyword && (String(item.id).includes(keyword) || String(item.name).includes(keyword)))
+ matchFilter(item) {
+ const { searchable, keyword } = this.state
+ return (
+ this.isRemote() ||
+ !searchable ||
+ !keyword ||
+ (searchable &&
+ keyword &&
+ (String(item.id).includes(keyword) ||
+ String(item.name).includes(keyword)))
+ )
}
- resetFocusedIndex (setState = true) {
+ resetFocusedIndex(setState = true) {
let focusedIndex = -1
this.state.dropdownItems.every(item => {
@@ -421,32 +479,32 @@ class Select extends Component {
}
return true
})
- setState && this.setState({
- focusedIndex
- })
+ setState &&
+ this.setState({
+ focusedIndex
+ })
return focusedIndex
}
- setFocusedIndex (focusedIndex) {
- this.setState({focusedIndex})
+ setFocusedIndex(focusedIndex) {
+ this.setState({ focusedIndex })
}
- moveFocusedIndex (direction) {
- let {
- focusedIndex
- } = this.state
- const {
- dropdownItems
- } = this.state
+ moveFocusedIndex(direction) {
+ let { focusedIndex } = this.state
+ const { dropdownItems } = this.state
if (direction === 'up') {
- dropdownItems.slice(0, focusedIndex).reverse().every(item => {
- focusedIndex--
- if (!item.disabled && this.matchFilter(item)) {
- return false
- }
- return true
- })
+ dropdownItems
+ .slice(0, focusedIndex)
+ .reverse()
+ .every(item => {
+ focusedIndex--
+ if (!item.disabled && this.matchFilter(item)) {
+ return false
+ }
+ return true
+ })
} else {
dropdownItems.slice(focusedIndex + 1).every(item => {
focusedIndex++
@@ -461,10 +519,8 @@ class Select extends Component {
})
}
- localeDatasProps (key) {
- const {
- localeDatas
- } = this.props
+ localeDatasProps(key) {
+ const { localeDatas } = this.props
if (this.props[key]) {
return this.props[key]
} else {
@@ -472,7 +528,7 @@ class Select extends Component {
}
}
- render () {
+ render() {
const {
mode,
showCheckAll,
@@ -504,10 +560,20 @@ class Select extends Component {
}
return (
-
-
{ this.selectInputContainer = node }}>
+
+
{
+ this.selectInputContainer = node
+ }}
+ >
{ this.selectInput = node }}
+ ref={node => {
+ this.selectInput = node
+ }}
mode={mode}
disabled={disabled}
searchable={searchable}
@@ -519,8 +585,8 @@ class Select extends Component {
multipleMode={multipleMode}
container={this.selectInputContainer}
moveFocusedIndex={this.moveFocusedIndex.bind(this)}
- onClick={()=>{
- if(this.props.open) {
+ onClick={() => {
+ if (this.props.open) {
this.handleInputClick()
}
onClick()
@@ -533,14 +599,14 @@ class Select extends Component {
onEnterSelect={this.onEnterSelect.bind(this)}
/>
- { children }
+ {children}
+
{loading && (
@@ -86,7 +90,8 @@ export default class SelectDropdown extends Component {
className={classNames('hi-select__dropdown--item', {
'is-active': isSelected,
'is-disabled': isDisabled,
- 'hi-select__dropdown--item-default': !item.children && !dropdownRender
+ 'hi-select__dropdown--item-default':
+ !item.children && !dropdownRender
})}
onClick={e => this.onClickOption(e, item, index)}
key={item.id}
diff --git a/components/select/select-legacy/SelectInput.js b/components/select/select-legacy/select-v1/SelectInput.js
similarity index 100%
rename from components/select/select-legacy/SelectInput.js
rename to components/select/select-legacy/select-v1/SelectInput.js
diff --git a/components/select/select-legacy/common.js b/components/select/select-legacy/select-v1/common.js
similarity index 100%
rename from components/select/select-legacy/common.js
rename to components/select/select-legacy/select-v1/common.js
diff --git a/components/select/select-legacy/index.js b/components/select/select-legacy/select-v1/index.js
similarity index 100%
rename from components/select/select-legacy/index.js
rename to components/select/select-legacy/select-v1/index.js
diff --git a/components/select/select-legacy/select-v2/Select.js b/components/select/select-legacy/select-v2/Select.js
new file mode 100644
index 000000000..6d1a826a9
--- /dev/null
+++ b/components/select/select-legacy/select-v2/Select.js
@@ -0,0 +1,693 @@
+import React, { Component } from 'react'
+import ReactDOM from 'react-dom'
+import classNames from 'classnames'
+import PropTypes from 'prop-types'
+import debounce from 'lodash/debounce'
+import cloneDeep from 'lodash/cloneDeep'
+import Popper from '../../../popper'
+import SelectInput from './SelectInput'
+import SelectDropdown from './SelectDropdown'
+import Provider from '../../../context'
+import fetchJsonp from 'fetch-jsonp'
+import qs from 'qs'
+import _ from 'lodash'
+class Select extends Component {
+ autoloadFlag = true // 第一次自动加载数据标识
+
+ static propTypes = {
+ type: PropTypes.oneOf(['single', 'multiple']),
+ multipleWrap: PropTypes.oneOf(['wrap', 'nowrap']),
+ data: PropTypes.array,
+ dataSource: PropTypes.oneOfType([PropTypes.object, PropTypes.func]),
+ defaultValue: PropTypes.oneOfType([
+ PropTypes.string,
+ PropTypes.array,
+ PropTypes.bool,
+ PropTypes.number
+ ]),
+ value: PropTypes.oneOfType([
+ PropTypes.string,
+ PropTypes.array,
+ PropTypes.bool,
+ PropTypes.number
+ ]),
+ showCheckAll: PropTypes.bool,
+ autoload: PropTypes.bool,
+ searchable: PropTypes.bool,
+ filterOption: PropTypes.func,
+ clearable: PropTypes.bool,
+ disabled: PropTypes.bool,
+ placeholder: PropTypes.string,
+ emptyContent: PropTypes.string,
+ optionWidth: PropTypes.number,
+ style: PropTypes.object,
+ onChange: PropTypes.func,
+ render: PropTypes.func,
+ open: PropTypes.bool
+ }
+
+ static defaultProps = {
+ data: [],
+ type: 'single',
+ multipleWrap: 'nowrap',
+ disabled: false,
+ clearable: true,
+ defaultValue: '',
+ autoload: false,
+ showCheckAll: false,
+ open: true,
+ onClick: () => {},
+ onBlur: () => {},
+ onFocus: () => {}
+ }
+
+ constructor (props) {
+ super(props)
+
+ const { data, value, defaultValue } = props
+ const dropdownItems = cloneDeep(data)
+ const initialValue = value === undefined ? defaultValue : value
+ const selectedItems = this.resetSelectedItems(
+ initialValue,
+ dropdownItems,
+ []
+ )
+
+ const searchable = this.getSearchable()
+ this.debouncedFilterItems = debounce(this.onFilterItems.bind(this), 300)
+ this.clickOutsideHandel = this.clickOutside.bind(this)
+
+ this.state = {
+ searchable,
+ queryLength: 1,
+ focusedIndex: 0,
+ selectedItems,
+ cacheSelectedItems: selectedItems,
+ dropdownItems,
+ dropdownShow: false,
+ fetching: false,
+ keyword: '',
+ filterText: '',
+ searchInput: {
+ width: 2
+ }
+ }
+ }
+
+ getChildContext () {
+ return {
+ component: this
+ }
+ }
+
+ componentWillMount () {
+ if (this.isRemote() && this.props.autoload) {
+ this.remoteSearch()
+ }
+ }
+
+ componentDidMount () {
+ window.addEventListener('click', this.clickOutsideHandel)
+ this.resetFocusedIndex()
+ }
+
+ componentWillUnmount () {
+ window.removeEventListener('click', this.clickOutsideHandel)
+ }
+
+ clickOutside (e) {
+ const selectInput = ReactDOM.findDOMNode(this.selectInput)
+ if (
+ (selectInput && selectInput.contains(e.target)) ||
+ (e.target.tagName === 'INPUT' &&
+ e.target.className.includes('hi-select__dropdown__searchbar--input'))
+ ) {
+ return
+ }
+ this.hideDropdown()
+ }
+
+ componentWillReceiveProps (nextProps) {
+ if (!_.isEqual(nextProps.data, this.props.data)) {
+ const selectedItems = this.resetSelectedItems(
+ nextProps.value || this.state.selectedItems,
+ nextProps.data,
+ []
+ )
+ this.setState({
+ selectedItems,
+ cacheSelectedItems: selectedItems,
+ dropdownItems: cloneDeep(nextProps.data)
+ })
+ } else {
+ if (!_.isEqual(nextProps.value, this.props.value)) {
+ const selectedItems = this.resetSelectedItems(
+ nextProps.value,
+ this.state.dropdownItems,
+ []
+ ) // 异步获取时会从内部改变dropdownItems,所以不能从list取
+ this.setState({
+ selectedItems,
+ cacheSelectedItems: selectedItems
+ })
+ }
+ }
+ }
+
+ getSearchable () {
+ const { searchable } = this.props
+
+ if (this.isRemote()) {
+ return true
+ }
+ return searchable
+ }
+
+ parseValue (value = this.props.value) {
+ if (Array.isArray(value)) {
+ return value.map(v => {
+ if (typeof v === 'object') {
+ return v.id
+ } else {
+ return v
+ }
+ })
+ } else {
+ return [value]
+ }
+ }
+
+ isRemote () {
+ const { dataSource, onSearch } = this.props
+ return onSearch || dataSource
+ }
+
+ resetSelectedItems (value, dropdownItems = [], reviceSelectedItems = []) {
+ const values = this.parseValue(value)
+ let selectedItems = []
+ dropdownItems.forEach(item => {
+ if (values.includes(item.id)) {
+ selectedItems.push(item)
+ }
+ })
+ return _.uniqBy(reviceSelectedItems.concat(selectedItems), 'id')
+ }
+
+ onEnterSelect () {
+ const { dropdownItems, focusedIndex } = this.state
+ const item = dropdownItems[focusedIndex]
+ this.onClickOption(item, focusedIndex)
+ }
+
+ onChange (selectedItems, changedItems, callback, cacheSelectedItems) {
+ const { onChange, value } = this.props
+ value === undefined &&
+ this.setState(
+ {
+ selectedItems
+ },
+ callback
+ )
+ const selectedIds = selectedItems.map(({ id }) => id)
+ onChange && onChange(selectedIds, changedItems)
+ }
+
+ checkAll (filterItems, e) {
+ // 全选
+ e && e.stopPropagation()
+
+ const { selectedItems } = this.state
+ let _selectedItems = [...selectedItems]
+ let changedItems = []
+ filterItems.forEach(item => {
+ if (!item.disabled && this.matchFilter(item)) {
+ if (
+ !_selectedItems.map(selectItem => selectItem.id).includes(item.id)
+ ) {
+ _selectedItems.push(item)
+ changedItems.push(item)
+ }
+ }
+ })
+ this.onChange(_selectedItems, changedItems, () => {}, _selectedItems)
+ }
+
+ onClickOption (item, index) {
+ if (!item || item.disabled) return
+
+ let selectedItems = this.state.selectedItems.concat()
+ let cacheSelectedItems = this.state.selectedItems.concat()
+ let focusedIndex = index
+
+ if (this.props.type === 'multiple') {
+ let itemIndex = this.state.selectedItems.findIndex(sItem => {
+ return sItem.id === item.id
+ })
+ if (itemIndex === -1) {
+ selectedItems.push(item)
+ if (
+ !cacheSelectedItems.map(cacheItem => cacheItem.id).includes(item.id)
+ ) {
+ cacheSelectedItems.push(item)
+ }
+ } else {
+ selectedItems.splice(itemIndex, 1)
+ }
+ } else {
+ selectedItems = [item]
+ this.setState({
+ cacheSelectedItems: [item]
+ })
+ }
+
+ this.onChange(
+ selectedItems,
+ item,
+ () => {
+ this.setState({
+ focusedIndex,
+ cacheSelectedItems:
+ this.props.type === 'multiple' ? cacheSelectedItems : [item]
+ })
+ },
+ this.props.type === 'multiple' ? cacheSelectedItems : [item]
+ )
+ if (this.props.type !== 'multiple') {
+ this.hideDropdown()
+ }
+ }
+
+ clearKeyword () {
+ this.setState(
+ {
+ keyword: ''
+ },
+ () => {
+ this.selectInput.clearInput()
+ }
+ )
+ }
+
+ handleInputClick = e => {
+ let { dropdownShow } = this.state
+ if (dropdownShow) {
+ this.hideDropdown()
+ return
+ }
+
+ !this.getSearchable() && this.selectInput.focus()
+ if (this.props.disabled) {
+ return
+ }
+
+ if (dropdownShow === false) {
+ this.showDropdown()
+ }
+ }
+
+ hideDropdown () {
+ this.state.dropdownShow === true &&
+ this.setState(
+ { dropdownShow: false, cacheSelectedItems: this.state.selectedItems },
+ () => {
+ this.clearKeyword()
+ }
+ )
+ }
+
+ showDropdown () {
+ this.state.dropdownShow === false && this.setState({ dropdownShow: true })
+ }
+
+ deleteItem (item) {
+ if (item.disabled) return
+ let selectedItems = this.state.selectedItems.concat()
+ const sIndex = selectedItems.findIndex((obj, index, arr) => {
+ return obj.id === item.id
+ })
+
+ selectedItems.splice(sIndex, 1)
+ this.onChange(
+ selectedItems,
+ item,
+ () => {
+ !this.getSearchable() && this.selectInput.focus()
+ },
+ selectedItems
+ )
+ }
+ // 全部删除
+ deleteAllItems () {
+ const { type } = this.props
+ const focusedIndex = this.resetFocusedIndex()
+ const changedItems = [...this.state.selectedItems]
+ this.onChange(
+ [],
+ type === 'multiple' ? changedItems : changedItems[0],
+ () => {
+ this.setState({ focusedIndex })
+ this.onFilterItems('')
+ },
+ []
+ )
+ }
+
+ remoteSearch (keyword) {
+ const { onSearch, dataSource, autoload } = this.props
+ if (onSearch && typeof onSearch === 'function') {
+ this.setState({
+ fetching: true
+ })
+ onSearch(keyword).finally(() => {
+ this.setState({ fetching: false })
+ })
+ } else {
+ const _dataSource =
+ typeof dataSource === 'function' ? dataSource(keyword) : dataSource
+ let {
+ url,
+ transformResponse,
+ error,
+ params,
+ headers,
+ mode,
+ data = {},
+ type = 'GET',
+ key,
+ jsonpCallback = 'callback',
+ ...options
+ } = _dataSource
+
+ keyword =
+ !keyword && this.autoloadFlag && autoload
+ ? _dataSource.keyword
+ : keyword
+ this.autoloadFlag = false // 第一次自动加载数据后,输入的关键词即使为空也不再使用默认关键词
+ Object.assign(options, { mode }, { headers })
+
+ const queryParams = qs.stringify(
+ Object.assign({}, params, key && { [key]: keyword })
+ )
+ if (!_.isEmpty(queryParams)) {
+ url = url.includes('?')
+ ? `${url}&${queryParams}`
+ : `${url}?${queryParams}`
+ }
+ if (type.toUpperCase() === 'POST') {
+ options.body = JSON.stringify(data)
+ }
+
+ this.setState({
+ fetching: true
+ })
+
+ if (type.toUpperCase() === 'JSONP') {
+ const _o = {
+ jsonpCallback: jsonpCallback,
+ jsonpCallbackFunction: jsonpCallback
+ }
+ fetchJsonp(url, _o)
+ .then(res => res.json())
+ .then(json => {
+ this._setDropdownItems(json, transformResponse)
+ })
+ } else {
+ /* eslint-disable */
+ fetch(url, {
+ method: type,
+ ...options
+ })
+ .then(response => response.json())
+ .then(
+ res => {
+ this._setDropdownItems(res, transformResponse)
+ },
+ err => {
+ error && error(err)
+ this.setState({
+ fetching: false
+ })
+ }
+ )
+ }
+ }
+ }
+ _setDropdownItems(res, func) {
+ let dropdownItems = []
+ if (func) {
+ dropdownItems = func(res)
+ } else {
+ dropdownItems = res.data
+ }
+ if (Array.isArray(dropdownItems)) {
+ const reviceSelectedItems =
+ this.props.type === 'multiple'
+ ? (this.props.dataSource && this.state.selectedItems) || []
+ : this.state.cacheSelectedItems
+ const selectedItems = this.resetSelectedItems(
+ this.props.value,
+ dropdownItems,
+ reviceSelectedItems
+ )
+ this.setState({
+ dropdownItems,
+ selectedItems
+ })
+ }
+ this.setState({
+ fetching: false
+ })
+ }
+ onFilterItems(keyword) {
+ const { onSearch, dataSource, autoload } = this.props
+ this.setState(
+ {
+ keyword: keyword
+ },
+ () => this.resetFocusedIndex()
+ )
+
+ if (dataSource) {
+ if (autoload || (keyword && keyword.length >= this.state.queryLength)) {
+ this.remoteSearch(keyword)
+ }
+ } else if (onSearch) {
+ this.remoteSearch(keyword)
+ }
+ }
+
+ matchFilter(item) {
+ const { filterOption } = this.props
+ const { searchable, keyword } = this.state
+
+ const shouldMatch = this.isRemote() || !searchable || !keyword
+
+ if (typeof filterOption === 'function') {
+ return shouldMatch || filterOption(keyword, item)
+ }
+
+ return (
+ shouldMatch ||
+ String(item.id).includes(keyword) ||
+ String(item.title).includes(keyword)
+ )
+ }
+
+ resetFocusedIndex(setState = true) {
+ let focusedIndex = -1
+
+ this.state.dropdownItems.every(item => {
+ focusedIndex++
+ if (!item.disabled && this.matchFilter(item)) {
+ return false
+ }
+ return true
+ })
+ setState &&
+ this.setState({
+ focusedIndex
+ })
+ return focusedIndex
+ }
+
+ setFocusedIndex(focusedIndex) {
+ this.setState({ focusedIndex })
+ }
+
+ moveFocusedIndex(direction) {
+ let { focusedIndex } = this.state
+ const { dropdownItems } = this.state
+
+ if (direction === 'up') {
+ dropdownItems
+ .slice(0, focusedIndex)
+ .reverse()
+ .every(item => {
+ focusedIndex--
+ if (!item.disabled && this.matchFilter(item)) {
+ return false
+ }
+ return true
+ })
+ } else {
+ dropdownItems.slice(focusedIndex + 1).every(item => {
+ focusedIndex++
+ if (!item.disabled && this.matchFilter(item)) {
+ return false
+ }
+ return true
+ })
+ }
+ this.setState({
+ focusedIndex
+ })
+ }
+
+ localeDatasProps(key) {
+ const { localeDatas } = this.props
+ if (this.props[key]) {
+ return this.props[key]
+ } else {
+ return localeDatas.select[key]
+ }
+ }
+
+ render() {
+ const {
+ type,
+ showCheckAll,
+ className,
+ disabled,
+ clearable,
+ style,
+ children,
+ optionWidth,
+ render,
+ multipleWrap,
+ onClick,
+ onBlur,
+ onFocus,
+ dataSource,
+ filterOption,
+ onSearch,
+ theme,
+ localeDatas,
+ preventOverflow,
+ placement
+ } = this.props
+ const placeholder = this.localeDatasProps('placeholder')
+ const emptyContent = this.localeDatasProps('emptyContent')
+ const searchPlaceholder = this.localeDatasProps('searchPlaceholder')
+ const {
+ selectedItems,
+ cacheSelectedItems,
+ dropdownItems,
+ searchable,
+ dropdownShow,
+ focusedIndex,
+ fetching
+ } = this.state
+ const extraClass = {
+ 'is-multiple': type === 'multiple',
+ 'is-single': type === 'single'
+ }
+ const selectInputWidth = this.selectInputContainer
+ ? this.selectInputContainer.getBoundingClientRect().width
+ : null
+ return (
+
+
{
+ this.selectInputContainer = node
+ }}
+ >
+ {
+ this.selectInput = node
+ }}
+ theme={theme}
+ mode={type}
+ disabled={disabled}
+ searchable={searchable}
+ clearable={clearable}
+ show={dropdownShow && this.props.open}
+ dropdownShow={dropdownShow}
+ placeholder={placeholder}
+ selectedItems={selectedItems}
+ dropdownItems={dropdownItems}
+ multipleMode={multipleWrap}
+ container={this.selectInputContainer}
+ moveFocusedIndex={this.moveFocusedIndex.bind(this)}
+ onClick={() => {
+ if (this.props.open) {
+ this.handleInputClick()
+ }
+ onClick()
+ }}
+ onBlur={onBlur}
+ onFocus={onFocus}
+ onDelete={this.deleteItem.bind(this)}
+ onClear={this.deleteAllItems.bind(this)}
+ onSearch={this.debouncedFilterItems.bind(this)}
+ onEnterSelect={this.onEnterSelect.bind(this)}
+ />
+
+ {children}
+
+ {dropdownShow && this.props.open && (
+
+ )}
+
+
+ )
+ }
+}
+Select.childContextTypes = {
+ component: PropTypes.any
+}
+
+export default Provider(Select)
diff --git a/components/select/select-legacy/select-v2/SelectDropdown.js b/components/select/select-legacy/select-v2/SelectDropdown.js
new file mode 100644
index 000000000..158b94aaf
--- /dev/null
+++ b/components/select/select-legacy/select-v2/SelectDropdown.js
@@ -0,0 +1,296 @@
+import React, { Component } from 'react'
+import classNames from 'classnames'
+import Checkbox from '../../../checkbox'
+import Loading from '../../../loading'
+import Icon from '../../../icon'
+
+class SelectDropdown extends Component {
+ constructor (props) {
+ super(props)
+ this.state = {
+ filterItems: this.props.dropdownItems,
+ searchbarValue: '',
+ cachedropdownItems: this.props.dropdownItems
+ }
+ }
+ static getDerivedStateFromProps (nextProps, prevState) {
+ const { selectedItems, mode, isOnSearch, dropdownItems, show } = nextProps
+ const { searchbarValue, cachedropdownItems } = prevState
+ const _filterItems =
+ selectedItems.length > 0 &&
+ searchbarValue.length === 0 &&
+ mode === 'single' &&
+ isOnSearch
+ ? cachedropdownItems
+ : dropdownItems
+ const _searchbarValue = show ? searchbarValue : ''
+ return { filterItems: _filterItems, searchbarValue: _searchbarValue }
+ }
+ componentDidMount () {
+ this.focus()
+ }
+ focus = () => {
+ this.props.searchable && setTimeout(() => this.searchbar.focus(), 0)
+ }
+ onClickOption (e, item, index) {
+ e.stopPropagation()
+ e.preventDefault()
+ if (item.disabled) {
+ return
+ }
+ this.props.mode === 'single' &&
+ this.props.isOnSearch &&
+ this.setState({
+ cachedropdownItems: this.props.dropdownItems
+ })
+ this.props.onClickOption(item, index)
+ }
+ filterOptions = keyword => {
+ const { dropdownItems, filterOption } = this.props
+ let filterItems = []
+ if (typeof filterOption === 'function' || keyword === '') {
+ filterItems = dropdownItems
+ } else {
+ dropdownItems.map(item => {
+ String(item.title).includes(keyword) && filterItems.push(item)
+ })
+ }
+ this.setState({
+ filterItems: filterItems,
+ searchbarValue: keyword
+ })
+ }
+ searchEvent (e) {
+ const filterText = e.target.value
+ this.filterOptions(filterText)
+ this.props.onSearch(filterText)
+
+ this.setState({
+ searchbarValue: filterText
+ })
+ }
+ hightlightKeyword (text, uniqueKey) {
+ const { searchbarValue } = this.state
+ let _keyword = this.state.searchbarValue
+ _keyword = searchbarValue.includes('[')
+ ? _keyword.replace(/\[/gi, '\\[')
+ : _keyword
+ _keyword = searchbarValue.includes('(')
+ ? _keyword.replace(/\(/gi, '\\(')
+ : _keyword
+ _keyword = searchbarValue.includes(')')
+ ? _keyword.replace(/\)/gi, '\\)')
+ : _keyword
+
+ let parts = text.split(new RegExp(`(${_keyword})`, 'gi'))
+ return this.state.searchbarValue.length > 0 ? (
+
+ {parts.map((part, i) =>
+ part === searchbarValue ? (
+
+ {part}
+
+ ) : (
+ part
+ )
+ )}
+
+ ) : (
+ text
+ )
+ }
+ onMouseEnter (item, index) {
+ !item.disabled && this.props.setFocusedIndex(index)
+ }
+
+ itemSelected (item) {
+ const selectedItems = this.props.selectedItems
+
+ return selectedItems.map(item => item.id).indexOf(item.id) > -1
+ }
+ cleanSearchbarValue (e) {
+ e.stopPropagation()
+ const filterText = ''
+ this.filterOptions(filterText)
+ this.props.onSearch(filterText)
+
+ this.setState({
+ searchbarValue: filterText
+ })
+ }
+ handleKeyDown (evt) {
+ if (evt.keyCode === 13) {
+ this.props.onEnterSelect()
+ }
+
+ if (evt.keyCode === 38) {
+ evt.preventDefault()
+ this.props.moveFocusedIndex('up')
+ }
+ if (evt.keyCode === 40) {
+ evt.preventDefault()
+ this.props.moveFocusedIndex('down')
+ }
+ }
+ renderOption (mode, isSelected, item) {
+ if (item.children) {
+ return item.children
+ }
+ if (this.props.dropdownRender) {
+ return this.props.dropdownRender(item, isSelected)
+ }
+ const paddingNum = mode === 'multiple' ? 48 : 24
+ const style = {
+ width: this.props.optionWidth
+ ? this.props.optionWidth - paddingNum
+ : this.props.selectInputWidth
+ ? this.props.selectInputWidth - paddingNum
+ : null
+ }
+
+ return (
+
+ {mode === 'multiple' && (
+
+
+ {this.props.isOnSearch
+ ? item.title
+ : this.hightlightKeyword(item.title, item.id)}
+
+
+ )}
+ {mode === 'single' && (
+
+ {this.props.isOnSearch
+ ? item.title
+ : this.hightlightKeyword(item.title, item.id)}
+
+ )}
+ {mode === 'single' && isSelected && (
+
+
+
+ )}
+
+ )
+ }
+
+ render () {
+ const {
+ mode,
+ focusedIndex,
+ matchFilter,
+ noFoundTip,
+ loading,
+ optionWidth,
+ showCheckAll,
+ dropdownRender,
+ theme,
+ searchable,
+ onFocus,
+ onBlur,
+ searchPlaceholder,
+ localeMap
+ } = this.props
+ const { filterItems, searchbarValue } = this.state
+ let matched = 0
+ const style = optionWidth && {
+ width: optionWidth
+ }
+
+ return (
+
+ {searchable && (
+
+
+
+ {
+ this.searchbar = input
+ }}
+ value={searchbarValue}
+ onFocus={onFocus.bind(this)}
+ onBlur={onBlur.bind(this)}
+ clearabletrigger='always'
+ onKeyDown={this.handleKeyDown.bind(this)}
+ onInput={this.searchEvent.bind(this)}
+ onChange={this.searchEvent.bind(this)}
+ />
+ {searchbarValue.length > 0 ? (
+
+ ) : null}
+
+
+ )}
+ {loading && (
+
+
+
+ )}
+ {!loading && (
+
+ {filterItems.map((item, index) => {
+ if (matchFilter(item)) {
+ matched++
+ const isSelected = this.itemSelected(item)
+ const isDisabled = item.disabled
+ return (
+ - this.onClickOption(e, item, index)}
+ key={item.id}
+ data-focused={focusedIndex === index}
+ onMouseEnter={() => this.onMouseEnter(item, index)}
+ >
+ {this.renderOption(mode, isSelected, item)}
+
+ )
+ }
+ })}
+ {matched === 0 && (
+ - e.stopPropagation()}
+ >
+ {noFoundTip}
+
+ )}
+
+ )}
+ {mode === 'multiple' && showCheckAll && (
+
+ {localeMap['checkAll']}
+
+ )}
+
+ )
+ }
+}
+
+export default SelectDropdown
diff --git a/components/select/select-legacy/select-v2/SelectInput.js b/components/select/select-legacy/select-v2/SelectInput.js
new file mode 100644
index 000000000..8b2c4037d
--- /dev/null
+++ b/components/select/select-legacy/select-v2/SelectInput.js
@@ -0,0 +1,310 @@
+import React, { Component } from 'react'
+import classNames from 'classnames'
+import { getTextWidth } from './common.js'
+
+class SelectInput extends Component {
+ constructor (props) {
+ super(props)
+
+ this.state = {
+ showCount: 0,
+ value: '',
+ inputStyle: {
+ width: 2
+ },
+ cacheselectedItems: []
+ }
+ }
+
+ calShowCountFlag = true
+ componentDidUpdate () {
+ if (
+ this.props.multipleMode === 'nowrap' &&
+ this.calShowCountFlag &&
+ this.itemsRef
+ ) {
+ // 多选超过一行时以数字显示
+ const itemsRect = this.itemsRef.getBoundingClientRect()
+ let width = 0
+ let showCount = 0
+ const items = this.itemsRef.querySelectorAll('.hi-select__input--item')
+
+ for (const item of items) {
+ const itemRect = item.getBoundingClientRect()
+ width += itemRect.width
+ if (
+ width + 50 < itemsRect.width ||
+ (width > itemsRect.width &&
+ width + 50 <= itemsRect.width &&
+ showCount + 1 === items.length)
+ ) {
+ // 50是留给显示剩余选项的空间
+ ++showCount
+ } else {
+ break
+ }
+ }
+ this.setState({ showCount })
+ this.calShowCountFlag = false
+ } else {
+ this.calShowCountFlag = true
+ }
+ }
+
+ static getDerivedStateFromProps (nextProps, nextState) {
+ return nextProps.dropdownShow
+ ? {
+ cacheselectedItems:
+ nextProps.selectedItems.length > 0
+ ? nextProps.selectedItems
+ : nextState.cacheselectedItems
+ }
+ : { cacheselectedItems: nextProps.selectedItems }
+ }
+ focus () {
+ setTimeout(() => this.searchInput && this.searchInput.focus(), 0)
+ }
+
+ handleKeywordChange (evt) {
+ var val = evt.target.value
+ this.setState({
+ value: val,
+ inputStyle: {
+ width: getTextWidth(val)
+ }
+ })
+ this.props.onSearch(evt.target.value)
+ }
+
+ clearInput () {
+ this.searchInput && (this.searchInput.value = '')
+ this.setState({
+ value: ''
+ })
+ }
+
+ handleKeyDown (evt) {
+ if (evt.keyCode === 13) {
+ this.props.onEnterSelect()
+ }
+
+ if (evt.keyCode === 38) {
+ evt.preventDefault()
+ this.props.moveFocusedIndex('up')
+ }
+ if (evt.keyCode === 40) {
+ evt.preventDefault()
+ this.props.moveFocusedIndex('down')
+ }
+ }
+
+ handleClear () {
+ this.setState({
+ cacheselectedItems: []
+ })
+ this.props.onClear()
+ this.clearInput()
+ }
+
+ renderMultiple () {
+ let {
+ placeholder,
+ selectedItems,
+ dropdownShow,
+ disabled,
+ searchable,
+ clearable,
+ multipleMode,
+ onFocus,
+ theme,
+ onBlur
+ } = this.props
+ let icon = dropdownShow ? 'up' : 'down'
+ let { showCount, value, inputStyle } = this.state
+ showCount =
+ showCount === 0 || this.calShowCountFlag
+ ? selectedItems.length
+ : showCount
+
+ if (!selectedItems.length) {
+ inputStyle = { width: '100%' }
+ }
+ return (
+
+ {selectedItems.length === 0 && !value && (
+
{placeholder}
+ )}
+
{
+ this.itemsRef = node
+ }}
+ >
+ {selectedItems.slice(0, showCount).map((item, index) => {
+ const _item = (
+
+
{item.title}
+
{
+ e.stopPropagation()
+ this.props.onDelete(item)
+ }}
+ >
+
+
+
+ )
+ return _item
+ })}
+ {showCount < selectedItems.length && (
+
+ +
+
+ {selectedItems.length - showCount}
+
+
+ )}
+ {searchable && !disabled && (
+
+ {
+ this.searchInput = input
+ }}
+ onChange={this.handleKeywordChange.bind(this)}
+ onKeyDown={this.handleKeyDown.bind(this)}
+ onFocus={onFocus.bind(this)}
+ onBlur={onBlur.bind(this)}
+ readOnly
+ />
+
+ )}
+
+
+ 0 }
+ )}
+ />
+ {clearable && selectedItems.length > 0 && (
+
+ )}
+
+
+ )
+ }
+
+ renderSingle () {
+ let {
+ placeholder,
+ selectedItems,
+ dropdownShow,
+ disabled,
+ clearable,
+ onFocus,
+ theme,
+ onBlur
+ } = this.props
+
+ selectedItems =
+ selectedItems.length > 0 ? selectedItems : this.state.cacheselectedItems
+ placeholder =
+ selectedItems.length > 0 ? selectedItems[0].title : placeholder
+ let icon = dropdownShow ? 'up' : 'down'
+
+ return (
+
+
0)
+ })}
+ >
+
+ {selectedItems[0] && selectedItems[0].title}
+
+
+ {(dropdownShow || selectedItems.length === 0) && (
+
0
+ })}
+ >
+ {
+ this.searchInput = input
+ }}
+ value={selectedItems.length > 0 ? placeholder : ''}
+ placeholder={placeholder}
+ onChange={this.handleKeywordChange.bind(this)}
+ onKeyDown={this.handleKeyDown.bind(this)}
+ onFocus={onFocus.bind(this)}
+ onBlur={onBlur.bind(this)}
+ readOnly
+ />
+
+ )}
+
+ 0 }
+ )}
+ />
+ {clearable && selectedItems.length > 0 && (
+
+ )}
+
+
+ )
+ }
+
+ render () {
+ let { mode } = this.props
+
+ if (mode === 'multiple') {
+ return this.renderMultiple()
+ } else {
+ return this.renderSingle()
+ }
+ }
+}
+
+export default SelectInput
diff --git a/components/select/select-legacy/select-v2/__tests__/index.test.js b/components/select/select-legacy/select-v2/__tests__/index.test.js
new file mode 100644
index 000000000..8164c2098
--- /dev/null
+++ b/components/select/select-legacy/select-v2/__tests__/index.test.js
@@ -0,0 +1,485 @@
+import React from 'react'
+import { mount, shallow } from 'enzyme'
+import { Simulate } from 'react-dom/test-utils'
+import sinon from 'sinon'
+
+import Select from '../'
+/* eslint-env jest */
+const changeCallback = jest.fn(items => items)
+const successCallback = jest.fn(res => res.data)
+const errorCallback = jest.fn(err => err)
+jest.mock('lodash/debounce', () => jest.fn(fn => fn))
+function trigger(elem, event, code ){
+
+ var myEvent = document.createEvent('Event') // 初始化这个事件对象,为它提高需要的“特性”
+ if (code) {
+ myEvent.which = code;
+ myEvent.keyCode = code; // Ctrl
+ }
+
+ myEvent.initEvent(event, true, true); //执行事件
+
+ elem.dispatchEvent(myEvent);
+
+}
+const options = [
+ { title: '电视', id: '3' },
+ { title: '手机', id: '2' },
+ { title: '笔记本', id: '4' },
+ { title: '生活周边', id: '5' },
+ { title: '办公', id: '6' }
+]
+const multiOptions = [
+ { title: '手机', id: '2' },
+ { title: '小米2', id: '2-1' },
+ { title: '小米3', id: '2-2' },
+ { title: '小米4', id: '2-3' },
+ { title: '小米5', id: '2-4' },
+ { title: '电脑', id: '3' },
+ { title: '笔记本', id: '4' },
+ { title: '生活周边', id: '5' },
+ { title: '其它', id: '6' }
+]
+const types = ['single', 'multiple']
+describe('Select', () => {
+ beforeEach(() => {
+ jest.useFakeTimers()
+ })
+
+ afterEach(() => {
+ jest.useRealTimers()
+ })
+
+ it('should disabled', () => {
+ types.forEach(type => {
+ const wrapper = mount(
)
+
+ expect(wrapper.find('.hi-select__input').hasClass('disabled')).toEqual(true)
+ wrapper.find('.hi-select__input').simulate('click')
+ expect(wrapper.find('.hi-select__popper').prop('show')).toEqual(false)
+ wrapper.unmount()
+ })
+ })
+
+ it('should have default value', () => {
+ types.forEach(type => {
+ const wrapper = mount(
+
+ )
+ if (type === 'single') {
+ expect(
+ wrapper.find('.hi-select__input').find('.hi-select__input--item__name').text()
+ ).toEqual('电视')
+ wrapper.find('.hi-select__input').simulate('click')
+ expect(wrapper.find('.hi-select__popper').prop('show')).toEqual(true)
+ document.querySelectorAll('.hi-select__dropdown--item')[2].click()
+ wrapper.update()
+ expect(wrapper.find('.hi-select__popper').prop('show')).toEqual(false)
+ expect(
+ wrapper.find('.hi-select__input').find('.hi-select__input--item__name').text()
+ ).toEqual('笔记本')
+ }
+
+ if (type === 'multiple') {
+ expect(
+ wrapper
+ .find('.hi-select__input')
+ .find('.hi-select__input--item')
+ .map(val => val.find('.hi-select__input--item__name').text())
+ ).toEqual(['手机', '电脑'])
+ wrapper.find('.hi-select__input').simulate('click')
+ expect(wrapper.find('.hi-select__popper').prop('show')).toEqual(true)
+ document
+ .querySelectorAll('.hi-select__dropdown')[0]
+ .querySelectorAll('.hi-select__dropdown--item')[2]
+ .click()
+ wrapper.update()
+ expect(wrapper.find('.hi-select__popper').prop('show')).toEqual(true)
+ expect(
+ wrapper
+ .find('.hi-select__input')
+ .find('.hi-select__input--item')
+ .map(val => val.find('.hi-select__input--item__name').text())
+ ).toEqual(['手机', '电脑', '小米3'])
+ }
+ wrapper.unmount()
+ })
+ })
+
+ it('should control by value', () => {
+ types.forEach(type => {
+ const wrapper = mount(
+
+ )
+ if (type === 'single') {
+ expect(
+ wrapper.find('.hi-select__input').find('.hi-select__input--item__name').text()
+ ).toEqual('手机')
+ wrapper.find('.hi-select__input').simulate('click')
+ expect(wrapper.find('.hi-select__popper').prop('show')).toEqual(true)
+ document.querySelectorAll('.hi-select__dropdown--item')[2].click()
+ expect(
+ wrapper.find('.hi-select__input').find('.hi-select__input--item__name').text()
+ ).toEqual('手机')
+ wrapper.setProps({
+ value: '6'
+ })
+ expect(
+ wrapper.find('.hi-select__input').find('.hi-select__input--item__name').text()
+ ).toEqual('办公')
+ }
+
+ if (type === 'multiple') {
+ expect(
+ wrapper
+ .find('.hi-select__input')
+ .find('.hi-select__input--item')
+ .map(val => val.find('.hi-select__input--item__name').text())
+ ).toEqual(['手机', '笔记本'])
+ wrapper.find('.hi-select__input').simulate('click')
+ expect(wrapper.find('.hi-select__popper').prop('show')).toEqual(true)
+ document
+ .querySelectorAll('.hi-select__dropdown')[0]
+ .querySelectorAll('.hi-select__dropdown--item')[2]
+ .click()
+ expect(
+ wrapper
+ .find('.hi-select__input')
+ .find('.hi-select__input--item')
+ .map(val => val.find('.hi-select__input--item__name').text())
+ ).toEqual(['手机', '笔记本'])
+ wrapper.setProps({
+ value: ['4', '5', '6']
+ })
+ expect(
+ wrapper
+ .find('.hi-select__input')
+ .find('.hi-select__input--item')
+ .map(val => val.find('.hi-select__input--item__name').text())
+ ).toEqual(['笔记本', '生活周边', '其它'])
+ }
+ wrapper.unmount()
+ })
+ })
+
+ it('should can delete one in multiple select', () => {
+ const wrapper = mount(
+
+ )
+ expect(wrapper.find('.hi-select__input--item')).toHaveLength(4)
+ wrapper.find('.hi-select__input--item__remove').at(0).simulate('click') // 删除第一个
+ expect(wrapper.find('.hi-select__input--item')).toHaveLength(3)
+ wrapper.unmount()
+ })
+
+ it('should can delete all in multiple select', () => {
+ const wrapper = mount(
+
+ )
+ expect(wrapper.find('.hi-select__input--item')).toHaveLength(4)
+ wrapper.find('.hi-select__input').simulate('focus')
+ wrapper.find('.hi-select__input--icon__close').simulate('click')
+ expect(wrapper.find('.hi-select__input--item')).toHaveLength(0)
+ wrapper.unmount()
+ })
+
+ it('should can check all in multiple select', () => {
+ const wrapper = mount(
+
+ )
+ wrapper.find('.hi-select__input').simulate('click')
+ expect(wrapper.find('.hi-select__popper').prop('show')).toEqual(true)
+ document.querySelector('.hi-select__dropdown__searchbar--input').value = '米'
+ trigger(document.querySelector('.hi-select__dropdown__searchbar--input'),'input')
+ document.querySelectorAll('.hi-select__dropdown-check-all')[0].click()
+ expect(document.querySelectorAll('.hi-checkbox__input--checked')).toHaveLength(4)
+ wrapper.unmount()
+ })
+
+ it('should custom render dropdown list', () => {
+ const wrapper = mount(
+