pull/9/head
tangjinzhou 2017-12-26 19:04:28 +08:00
parent 2e07f98d40
commit b9fe970682
14 changed files with 560 additions and 71 deletions

View File

@ -1,12 +1,13 @@
import clonedeep from 'lodash.clonedeep'
export function cloneVNode (vnode, deep) {
const cloned = new vnode.constructor(
vnode.tag,
vnode.data,
clonedeep(vnode.data),
vnode.children,
vnode.text,
vnode.elm,
vnode.context,
vnode.componentOptions,
clonedeep(vnode.componentOptions),
vnode.asyncFactory
)
cloned.ns = vnode.ns
@ -30,7 +31,7 @@ export function cloneVNodes (vnodes, deep) {
}
export function cloneElement (node, nodeProps) {
const { props, key, ref } = nodeProps
const { props, key } = nodeProps
if (node.componentOptions) {
Object.assign(node.componentOptions.propsData, props)
}
@ -40,7 +41,7 @@ export function cloneElement (node, nodeProps) {
attrs = data.attrs,
on = data.on,
} = nodeProps
Object.assign(node.data, { style, attrs, class: cls, on })
node.data = Object.assign(data, { style, attrs, class: cls, on })
if (key !== undefined) {
node.key = key
}

View File

@ -1,11 +1,14 @@
import warning from 'warning'
const warned = {}
export default (valid, message) => {
export default (valid, message, throwError) => {
if (process.env.NODE_ENV !== 'production') {
if (!valid && !warned[message]) {
warning(false, message)
warned[message] = true
if (throwError) {
throw Error(message)
}
}
}
}

View File

@ -1,5 +1,5 @@
<script>
import PropTypes from 'vue-types'
import PropTypes from '../_util/vue-types'
import align from 'dom-align'
import clonedeep from 'lodash.clonedeep'
import addEventListener from '../_util/Dom/addEventListener'

View File

@ -39,7 +39,7 @@ export default {
return (
<div
style={{
margin: 50,
margin: '50px',
}}
>
<p>

View File

@ -43,6 +43,8 @@ export default {
const { value } = this.$props
if (value === undefined) {
this.stateValue = e.target.value
this.$emit('change.value', e.target.value)
this.$emit('change', e)
} else {
this.$forceUpdate()
this.$emit('change.value', e.target.value)

View File

@ -209,7 +209,7 @@
// Animation
@animation-duration-slow: .3s; // Modal
@animation-duration-base: .2s;
@animation-duration-base: 20s;
@animation-duration-fast: .1s; // Tooltip
// Form

View File

@ -1,5 +1,5 @@
<script>
import PropTypes from 'vue-types'
import PropTypes from '../_util/vue-types'
export default {
props: {

View File

@ -13,11 +13,18 @@ export default {
destroyPopupOnHide: PropTypes.bool,
prefixCls: PropTypes.string,
getContainer: PropTypes.func,
transitionName: PropTypes.string,
animation: PropTypes.any,
maskAnimation: PropTypes.string,
maskTransitionName: PropTypes.string,
mask: PropTypes.bool,
zIndex: PropTypes.number,
},
mounted () {
this.rootNode = this.getPopupDomNode()
this._container = this.getContainer()
this._container.appendChild(this.$el)
// this._container.appendChild(this.$el)
// this.$refs.alignInstance.forceAlign()
},
beforeDestroy () {
this._container && this._container.parentNode.removeChild(this._container)
@ -37,7 +44,7 @@ export default {
},
getPopupDomNode () {
return this.$refs.popupInstance
return this.$refs.popupInstance ? this.$refs.popupInstance.$el : null
},
getTarget () {
@ -60,7 +67,7 @@ export default {
if (!transitionName && props.animation) {
transitionName = `${props.prefixCls}-${props.animation}`
}
return transitionName
return 'fade'
},
getClassName (currentAlignClassName) {
@ -116,16 +123,16 @@ export default {
</Align>) : null}
</transition>)
}
popupInnerProps.props.hiddenClassName = hiddenClassName
popupInnerProps.props.hiddenClassName = 'hiddenClassName'
return (<transition
name={this.getTransitionName()}
>
<Align
v-show={visible}
target={this.getTarget}
key='popup'
ref='alignInstance'
monitorWindowResize
childrenProps={{ visible }}
disabled={!visible}
align={align}
onAlign={this.onAlign}
@ -181,9 +188,20 @@ export default {
<div>
{this.getMaskElement()}
{this.getPopupElement()}
<transition name='fade'>
<div v-show={this.visible}>hello</div>
</transition>
</div>
)
},
// render () {
// return (
// <transition name='fade'>
// <div v-show={this.visible}>hello</div>
// </transition>
// )
// },
}
</script>

View File

@ -1,5 +1,5 @@
<script>
import PropTypes from 'vue-types'
import PropTypes from '../_util/vue-types'
import LazyRenderBox from './LazyRenderBox'
export default {

View File

@ -1,5 +1,10 @@
@triggerPrefixCls: rc-trigger-popup;
.fade-enter-active, .fade-leave-active {
transition: opacity 0.5s;
}
.fade-enter, .fade-leave-to /* .fade-leave-active in below version 2.1.8 */ {
opacity: 0;
}
.@{triggerPrefixCls} {
position: absolute;
left: -9999px;

View File

@ -36,19 +36,49 @@ const popupBorderStyle = {
export default {
render () {
return (
<div>
<div style={{ margin: '50px' }}>
<div style={{ margin: '200px' }}>
<div>
<Trigger
popupPlacement='right'
popupPlacement='left'
action={['click']}
builtinPlacements={builtinPlacements}
>
<div slot='popup' style={popupBorderStyle}>i am a click popup</div>
<span>
<div ref='saveContainerRef1' />
<Trigger
popupPlacement='bottom'
action={['hover']}
builtinPlacements={builtinPlacements}
getPopupContainer={() => this.$refs.saveContainerRef1}
>
<div slot='popup' style={popupBorderStyle}>i am a hover popup</div>
<span href='#' style={{ margin: '20px' }}>trigger</span>
</Trigger>
</span>
</Trigger>
</div>
{ false ? <div style={{ margin: '50px' }}>
<Trigger
popupPlacement='right'
action={['click', 'hover']}
builtinPlacements={builtinPlacements}
>
<div style={popupBorderStyle} slot='popup' onClick={(e) => { e.stopPropagation() }}>
jjj
<div ref='saveContainerRef' />
<Trigger
popupPlacement='bottom'
action={['click']}
builtinPlacements={builtinPlacements}
getPopupContainer={() => this.$refs.saveContainerRef}
>
<div slot='popup' style={popupBorderStyle}>I am inner Trigger Popup</div>
<span href='#' style={{ margin: 20 }}>clickToShowInnerTrigger</span>
</Trigger>
</div>
<span href='#' style={{ margin: '20px' }} onClick={console.log}>trigger</span>
</Trigger>
</div>
</div> : null}
</div>
)
},

View File

@ -0,0 +1,279 @@
<script>
import Trigger from '../index'
import '../assets/index.less'
import { Input, Button, Checkbox } from 'antd'
function getPopupAlign (state) {
return {
offset: [state.offsetX, state.offsetY],
overflow: {
adjustX: 1,
adjustY: 1,
},
}
}
const builtinPlacements = {
left: {
points: ['cr', 'cl'],
},
right: {
points: ['cl', 'cr'],
},
top: {
points: ['bc', 'tc'],
},
bottom: {
points: ['tc', 'bc'],
},
topLeft: {
points: ['bl', 'tl'],
},
topRight: {
points: ['br', 'tr'],
},
bottomRight: {
points: ['tr', 'br'],
},
bottomLeft: {
points: ['tl', 'bl'],
},
}
function preventDefault (e) {
e.preventDefault()
}
function getPopupContainer (trigger) {
return trigger.parentNode
}
export default {
data () {
return {
mask: false,
maskClosable: false,
placement: 'right',
trigger: {
hover: 1,
},
offsetX: undefined,
offsetY: undefined,
transitionName: '',
destroyPopupOnHide: false,
}
},
methods: {
setState (state, callback) {
Object.assign(this.$data, state)
this.$nextTick(() => {
callback && callback()
})
},
onPlacementChange (e) {
this.setState({
placement: e.target.value,
})
},
onTransitionChange (e) {
this.setState({
transitionName: e.target.checked ? e.target.value : '',
})
},
onTriggerChange (e) {
const trigger = Object.assign({}, this.$data.trigger)
if (e.target.checked) {
trigger[e.target.value] = 1
} else {
delete trigger[e.target.value]
}
this.setState({
trigger,
})
},
onOffsetXChange (e) {
const targetValue = e.target.value
this.setState({
offsetX: targetValue || undefined,
})
},
onOffsetYChange (e) {
const targetValue = e.target.value
this.setState({
offsetY: targetValue || undefined,
})
},
onVisibleChange (visible) {
console.log('tooltip', visible)
},
onMask (e) {
this.setState({
mask: e.target.checked,
})
},
onMaskClosable (e) {
this.setState({
maskClosable: e.target.checked,
})
},
destroy () {
this.setState({
destroyed: true,
})
},
handleDestroyPopupOnHide (e) {
this.setState({
destroyPopupOnHide: e.target.checked,
})
},
},
render () {
const state = this.$data
const trigger = state.trigger
if (state.destroyed) {
return null
}
return (<div >
<div style={{ margin: '10px 20px' }}>
<label>
placement:
<select value={state.placement} onChange={this.onPlacementChange}>
<option>right</option>
<option>left</option>
<option>top</option>
<option>bottom</option>
<option>topLeft</option>
<option>topRight</option>
<option>bottomRight</option>
<option>bottomLeft</option>
</select>
</label>
&nbsp;&nbsp;&nbsp;&nbsp;
<label>
<Checkbox
value='rc-trigger-popup-zoom'
onChange={this.onTransitionChange}
checked={state.transitionName === 'rc-trigger-popup-zoom'}
/>
transitionName
</label>
&nbsp;&nbsp;&nbsp;&nbsp;
trigger:
<label>
<Checkbox
value='hover'
checked={!!trigger.hover}
onChange={this.onTriggerChange}
/>
hover
</label>
<label>
<Checkbox
value='focus'
checked={!!trigger.focus}
onChange={this.onTriggerChange}
/>
focus
</label>
<label>
<Checkbox
value='click'
checked={!!trigger.click}
onChange={this.onTriggerChange}
/>
click
</label>
<label>
<Checkbox
value='contextMenu'
checked={!!trigger.contextMenu}
onChange={this.onTriggerChange}
/>
contextMenu
</label>
&nbsp;&nbsp;&nbsp;&nbsp;
<label>
<Checkbox
checked={!!state.destroyPopupOnHide}
onChange={this.handleDestroyPopupOnHide}
/>
destroyPopupOnHide
</label>
&nbsp;&nbsp;&nbsp;&nbsp;
<label>
<Checkbox
checked={!!state.mask}
onChange={this.onMask}
/>
mask
</label>
&nbsp;&nbsp;&nbsp;&nbsp;
<label>
<Checkbox
checked={!!state.maskClosable}
onChange={this.onMaskClosable}
/>
maskClosable
</label>
<br />
<label>
offsetX:
<Input
type='text'
onChange={this.onOffsetXChange}
style={{ width: '50px' }}
/>
</label>
&nbsp;&nbsp;&nbsp;&nbsp;
<label>
offsetY:
<Input
type='text'
onChange={this.onOffsetYChange}
style={{ width: '50px' }}
/>
</label>
&nbsp;&nbsp;&nbsp;&nbsp;
<Button onClick={this.destroy}>destroy</Button>
</div>
<div style={{ margin: '100px', position: 'relative' }}>
<Trigger
getPopupContainer={undefined && getPopupContainer}
popupAlign={getPopupAlign(state)}
popupPlacement={state.placement}
destroyPopupOnHide={state.destroyPopupOnHide}
// zIndex={40}
mask={state.mask}
maskClosable={state.maskClosable}
// maskAnimation="fade"
// mouseEnterDelay={0.1}
// mouseLeaveDelay={0.1}
action={Object.keys(state.trigger)}
builtinPlacements={builtinPlacements}
popupTransitionName={state.transitionName}
>
<div slot='popup' style={{ border: '1px solid red', padding: '10px', background: 'white' }}>
i am a popup
</div>
<a href='#' style={{ margin: '20px' }} onClick={preventDefault}>trigger</a>
</Trigger>
</div>
</div>)
},
}
</script>

160
components/trigger/index.md Normal file
View File

@ -0,0 +1,160 @@
## API
### props
<table class="table table-bordered table-striped">
<thead>
<tr>
<th style="width: 100px;">name</th>
<th style="width: 50px;">type</th>
<th style="width: 50px;">default</th>
<th>description</th>
</tr>
</thead>
<tbody>
<tr>
<td>popupClassName</td>
<td>string</td>
<td></td>
<td>additional className added to popup</td>
</tr>
<tr>
<td>forceRender</td>
<td>boolean</td>
<td>false</td>
<td>whether render popup before first show</td>
</tr>
<tr>
<td>destroyPopupOnHide</td>
<td>boolean</td>
<td>false</td>
<td>whether destroy popup when hide</td>
</tr>
<tr>
<td>getPopupClassNameFromAlign</td>
<td>getPopupClassNameFromAlign(align: Object):String</td>
<td></td>
<td>additional className added to popup according to align</td>
</tr>
<tr>
<td>action</td>
<td>string[]</td>
<td>['hover']</td>
<td>which actions cause popup shown. enum of 'hover','click','focus','contextMenu'</td>
</tr>
<tr>
<td>mouseEnterDelay</td>
<td>number</td>
<td>0</td>
<td>delay time to show when mouse enter. unit: s.</td>
</tr>
<tr>
<td>mouseLeaveDelay</td>
<td>number</td>
<td>0.1</td>
<td>delay time to hide when mouse leave. unit: s.</td>
</tr>
<tr>
<td>popupStyle</td>
<td>Object</td>
<td></td>
<td>additional style of popup</td>
</tr>
<tr>
<td>prefixCls</td>
<td>String</td>
<td>rc-trigger-popup</td>
<td>prefix class name</td>
</tr>
<tr>
<td>popupTransitionName</td>
<td>String|Object</td>
<td></td>
<td>https://github.com/react-component/animate</td>
</tr>
<tr>
<td>maskTransitionName</td>
<td>String|Object</td>
<td></td>
<td>https://github.com/react-component/animate</td>
</tr>
<tr>
<td>mask</td>
<td>boolean</td>
<td>false</td>
<td>whether to support mask</td>
</tr>
<tr>
<td>maskClosable</td>
<td>boolean</td>
<td>true</td>
<td>whether to support click mask to hide</td>
</tr>
<tr>
<td>popupVisible</td>
<td>boolean</td>
<td></td>
<td>whether popup is visible</td>
</tr>
<tr>
<td>zIndex</td>
<td>number</td>
<td></td>
<td>popup's zIndex</td>
</tr>
<tr>
<td>defaultPopupVisible</td>
<td>boolean</td>
<td></td>
<td>whether popup is visible initially</td>
</tr>
<tr>
<td>popupAlign</td>
<td>Object: alignConfig of [dom-align](https://github.com/yiminghe/dom-align)</td>
<td></td>
<td>popup 's align config</td>
</tr>
<tr>
<td>getPopupContainer</td>
<td>getPopupContainer(): HTMLElement</td>
<td></td>
<td>function returning html node which will act as popup container</td>
</tr>
<tr>
<td>getDocument</td>
<td>getDocument(): HTMLElement</td>
<td></td>
<td>function returning document node which will be attached click event to close trigger</td>
</tr>
<tr>
<td>popupPlacement</td>
<td>string</td>
<td></td>
<td>use preset popup align config from builtinPlacements, can be merged by popupAlign prop</td>
</tr>
<tr>
<td>builtinPlacements</td>
<td>object</td>
<td></td>
<td>builtin placement align map. used by placement prop</td>
</tr>
<tr>
<td>popupVisibleChange</td>
<td>$emit(visible)</td>
<td></td>
<td>call when popup visible is changed</td>
</tr>
<tr>
<td>popupAlign</td>
<td>$emit(popupDomNode, align)</td>
<td></td>
<td>callback when popup node is aligned</td>
</tr>
<tr>
<td>popup</td>
<td>slot='popup'</td>
<td></td>
<td>popup content</td>
</tr>
</tbody>
</table>

View File

@ -2,11 +2,11 @@
import PropTypes from '../_util/vue-types'
import contains from '../_util/Dom/contains'
import addEventListener from '../_util/Dom/addEventListener'
import warning from '../_util/warning'
import Popup from './Popup'
import { getAlignFromPlacement, getPopupClassNameFromAlign } from './utils'
import getContainerRenderMixin from '../_util/getContainerRenderMixin'
import StateMixin from '../_util/StateMixin'
import { cloneElement } from '../_util/vnode'
import { cloneElement, cloneVNode } from '../_util/vnode'
function returnEmptyString () {
return ''
@ -19,26 +19,6 @@ function returnDocument () {
const ALL_HANDLERS = ['click', 'mousedown', 'touchStart', 'mouseenter',
'mouseleave', 'focus', 'blur', 'contextMenu']
const mixins = []
mixins.push(
getContainerRenderMixin({
autoMount: false,
isVisible (instance) {
return instance.$data.sPopupVisible
},
isForceRender (instance) {
return instance.$props.forceRender
},
getContainer (instance) {
return instance.getContainer()
},
})
)
export default {
name: 'Trigger',
props: {
@ -65,7 +45,7 @@ export default {
zIndex: PropTypes.number,
focusDelay: PropTypes.number.def(0),
blurDelay: PropTypes.number.def(0.15),
getPopupContainer: Function,
getPopupContainer: PropTypes.func,
getDocument: PropTypes.func.def(returnDocument),
forceRender: PropTypes.bool,
destroyPopupOnHide: PropTypes.bool.def(false),
@ -74,15 +54,15 @@ export default {
// onPopupAlign: PropTypes.func,
popupAlign: PropTypes.object.def({}),
popupVisible: PropTypes.bool,
// defaultPopupVisible: PropTypes.bool.def(false),
// maskTransitionName: PropTypes.oneOfType([
// PropTypes.string,
// PropTypes.object,
// ]),
// maskAnimation: PropTypes.string,
defaultPopupVisible: PropTypes.bool.def(false),
maskTransitionName: PropTypes.oneOfType([
PropTypes.string,
PropTypes.object,
]),
maskAnimation: PropTypes.string,
},
mixins: [StateMixin, ...mixins],
mixins: [StateMixin],
data () {
const props = this.$props
let popupVisible
@ -184,9 +164,9 @@ export default {
onPopupMouseleave (e) {
if (e.relatedTarget && !e.relatedTarget.setTimeout &&
this._component &&
this._component.getPopupDomNode &&
contains(this._component.getPopupDomNode(), e.relatedTarget)) {
this.$refs.popup &&
this.$refs.popup.getPopupDomNode &&
contains(this.$refs.popup.getPopupDomNode(), e.relatedTarget)) {
return
}
this.delaySetPopupVisible(false, this.$props.mouseLeaveDelay)
@ -271,8 +251,8 @@ export default {
},
getPopupDomNode () {
// for test
if (this._component && this._component.getPopupDomNode) {
return this._component.getPopupDomNode()
if (this.$refs.popup && this.$refs.popup.getPopupDomNode) {
return this.$refs.popup.getPopupDomNode()
}
return null
},
@ -426,15 +406,17 @@ export default {
createTwoChains (event) {
const child = this.$slots.default[0]
let fn = () => {}
if (child && child.data && child.data.on) {
const childEvents = child.data.on
const events = (this.data ? this.data.on : {}) || {}
if (childEvents[event] && events[event]) {
return this[`fire${event}`]
}
fn = childEvents[event] || events[event] || fn
let fn = () => {
console.log('event', event)
}
child.data = child.data || {}
child.data.on = child.data.on || {}
const childEvents = child.data.on
const events = (this.data ? this.data.on : {}) || {}
if (childEvents[event] && events[event]) {
return this[`fire${event}`]
}
fn = childEvents[event] || events[event] || fn
return fn
},
@ -473,14 +455,15 @@ export default {
return action.indexOf('focus') !== -1 || hideAction.indexOf('blur') !== -1
},
forcePopupAlign () {
if (this.$data.sPopupVisible && this._component && this._component.$refs.alignInstance) {
this._component.$refs.alignInstance.forceAlign()
if (this.$data.sPopupVisible && this.$refs.popup && this.$refs.popup.$refs.alignInstance) {
this.$refs.popup.$refs.alignInstance.forceAlign()
}
},
fireEvents (type, e) {
const child = this.$slots.default[0]
if (child && child.data && child.on && child.on[type]) {
child.on[type](e)
if (child && child.data && child.data.on && child.data.on[type]) {
console.log(type, child.data.on[type])
// child.data.on[type](e)
}
if (this.data && this.data.on && this.data.on[type]) {
this.data.on[type](e)
@ -493,6 +476,9 @@ export default {
},
render () {
const children = this.$slots.default
if (children.length > 1) {
warning(false, 'Trigger $slots.default.length > 1, just support only one default', true)
}
const child = children[0]
const newChildProps = {
props: {},
@ -533,12 +519,17 @@ export default {
newChildProps.on.blur = this.createTwoChains('blur')
}
const trigger = cloneElement(child, newChildProps)
const trigger = cloneElement(cloneVNode(child), newChildProps)
const { sPopupVisible, forceRender } = this
if (sPopupVisible || forceRender || this._component) {
this._component = this.getComponent()
} else {
this._component = null
}
return (
<span>
{trigger}
{this.getComponent()}
{this._component}
</span>
)
},