@unpourtous/react-native-popup-stub
Advanced tools
Comparing version 1.0.19 to 1.1.0
{ | ||
"name": "@unpourtous/react-native-popup-stub", | ||
"version": "1.0.19", | ||
"version": "1.1.0", | ||
"description": "A popup container for implements your own popups like ActionSheet, Dialog, Alert, Toast ...", | ||
@@ -5,0 +5,0 @@ "main": "index.js", |
281
PopupStub.js
@@ -8,7 +8,11 @@ /* global __DEV__ */ | ||
import React, { Component } from 'react' | ||
import { View, TouchableWithoutFeedback, StyleSheet } from 'react-native' | ||
import { Platform, View, TouchableWithoutFeedback, StyleSheet } from 'react-native' | ||
import PropTypes from 'prop-types' | ||
import uuidV1 from 'uuid/v1' | ||
import * as Animatable from 'react-native-animatable' | ||
import createPopup from './util/createPopup' | ||
import log from './util/log' | ||
import { reverseKeyframes } from './util/keyframes' | ||
const IS_ANDROID = Platform.OS === 'android' | ||
const BG_FROM = 'rgba(0,0,0,0)' | ||
@@ -41,2 +45,29 @@ const BG_TO = 'rgba(23,26,35,0.6)' | ||
// static method is easier to use | ||
// these are the same api to non-static methods | ||
static addPopup (element, option) { | ||
if (!element || !PopupStub.stub) return | ||
return PopupStub.stub.addPopup(element, option) | ||
} | ||
static removePopup (id, forceUpdate = true) { | ||
if (!id || !PopupStub.stub) return | ||
PopupStub.stub.removePopup(id, forceUpdate) | ||
} | ||
static removePopupImmediately (id) { | ||
if (!id || !PopupStub.stub) return null | ||
return PopupStub.stub.removePopupImmediately(id) | ||
} | ||
static resetPopupProperty (id, key, value) { | ||
if (!id || !PopupStub.stub) return null | ||
PopupStub.stub.resetPopupProperty(id, key, value) | ||
} | ||
constructor (props) { | ||
@@ -46,16 +77,17 @@ super(props) | ||
this.state = { | ||
popups: new Map(), | ||
// if true, show a background visual mask | ||
hasMask: true, | ||
// visual mask related animation | ||
maskDelay: 0, | ||
maskDirection: 'normal', | ||
// background mask fades in/out | ||
maskAnimation: { | ||
from: { backgroundColor: BG_FROM }, | ||
to: { backgroundColor: props.maskColor } | ||
} | ||
popups: new Map() | ||
} | ||
} | ||
_sortPopups (popups) { | ||
if (popups.size > 1) { | ||
// sort by zIndex | ||
return new Map([...popups.entries()].sort((a, b) => { | ||
return a[1].zIndex - b[1].zIndex | ||
})) | ||
} | ||
return popups | ||
} | ||
/* | ||
@@ -65,11 +97,3 @@ * Add a new popup | ||
* @param {Component} element | ||
* @param {Object} [option] ```{ | ||
id: popup global unique id. | ||
lock: nearly same as pointerEvents, by default, 'auto' if has a mask, otherwise 'mask-only'. | ||
mask: has a mask or not, default true. | ||
zIndex: same as in css, the priority of popup, the bigger the higher. | ||
position: position of element in screen, available: none, left, right, top, bottom, center(defualt). | ||
wrapperStyle: animation wrapper style (each popup is wrapped in an Animatable.View). | ||
...[Animatable.props](https://github.com/oblador/react-native-animatable) direction and onAnimationEnd are reserved | ||
}``` | ||
* @param {Object} [option] | ||
* @return {String} popup unique id | ||
@@ -80,35 +104,12 @@ */ | ||
if (option && typeof option === 'string') { | ||
// This warning is for original version, and will be removed in future | ||
log('`id` parameter is deprecated, use `option` instead.', true) | ||
option = {id: option} | ||
} | ||
let opt = Object.assign({id: uuidV1(), zIndex: 1, mask: true, duration: 1000}, option) | ||
if (!opt.lock) { | ||
opt.lock = opt.mask ? 'auto' : 'mask-only' | ||
} else if (opt.lock === 'box-none' || opt.lock === 'box-only') { | ||
// sorry to misuse lock with pointerEvents, this will be removed in future | ||
log('`box-none` and `box-only` is deprecated, use `mask-only` and `all` instead.', true) | ||
} else if (['all', 'auto', 'mask-only', 'none'].indexOf(opt.lock) < 0) { | ||
log('lock should be all, auto, mask-only or none', true) | ||
} | ||
let newPopup = createPopup(element, option, uuidV1()) | ||
let popups = this.state.popups | ||
let newPopups = this.state.popups | ||
// close previous popup that has the same zIndex | ||
for (let key of newPopups.keys()) { | ||
let popup = newPopups.get(key) | ||
if (popup.zIndex === opt.zIndex && !popup._closing) { | ||
// TODO: enable config to close or not | ||
for (let key of popups.keys()) { | ||
let popup = popups.get(key) | ||
if (popup.zIndex === newPopup.zIndex && !popup._closing) { | ||
// new popup with same zIndex comes in with delay, visually | ||
opt.delay = popup.duration / 2 | ||
opt.onAnimationEnd = () => { | ||
// reset property | ||
let popups = this.state.popups | ||
let popup = popups.get(key) | ||
if (popup) { | ||
popup.delay = 0 | ||
popup.onAnimationEnd = null | ||
this.setState({popups}) | ||
} | ||
} | ||
newPopup.delay = popup.duration / 2 | ||
// remove popup until it completes animation | ||
@@ -122,19 +123,14 @@ this.removePopup(key, false) | ||
// add this new popup to our list | ||
opt.component = element | ||
newPopups.set(opt.id, opt) | ||
popups.set(newPopup.id, newPopup) | ||
log('adding...' + opt.id) | ||
this.setState({ | ||
// whenever a popup is added, reset background mask config | ||
hasMask: hasMask(newPopups), | ||
maskDelay: 0, | ||
maskDirection: 'normal', | ||
// We can't use position-zIndex here to identify the layer, | ||
// cause it has compatible problem in some android devices, | ||
// cause it has compatible problem in android devices, | ||
// so sort by hand. | ||
popups: sortPopups(newPopups) | ||
popups: this._sortPopups(popups) | ||
}) | ||
return opt.id | ||
log('added ' + newPopup.id) | ||
return newPopup.id | ||
} | ||
@@ -150,15 +146,15 @@ | ||
removePopup (id, forceUpdate = true) { | ||
// temporarily disable playback animations | ||
return this.removePopupImmediately(id) | ||
if (!id || !PopupStub.stub) return | ||
// TODO: fix playback animation in android | ||
if (IS_ANDROID) { | ||
return this.removePopupImmediately(id) | ||
} | ||
log('lazy closing...' + id) | ||
let popups = this.state.popups | ||
let popup = popups.get(id) | ||
if (!popup || popup._closing) return | ||
if (!popup || popup._closing) { | ||
return | ||
} | ||
// if no animation defined, remove it directly | ||
// TODO: There is a weird timing issue in android, so disable the animation temporarily | ||
if (!popup.animation) { | ||
@@ -169,8 +165,8 @@ this.removePopupImmediately(id) | ||
log('closing...' + id) | ||
// set close flag | ||
popup._closing = true | ||
// prevent double click | ||
popup.lock = 'all' | ||
// and try to activate background mask animation | ||
popup.mask = false | ||
// reset delay | ||
popup.delay = 0 | ||
@@ -194,9 +190,3 @@ // when closing a popup, it plays back | ||
if (forceUpdate) { | ||
let isLast = popups.size === 1 | ||
this.setState({ | ||
popups: popups, | ||
// if this is the last popup, prepare for fading out of background mask | ||
maskDirection: isLast ? 'reverse' : this.state.maskDirection, | ||
maskDelay: isLast ? Math.max(0, popup.duration - 100) : this.state.maskDelay | ||
}) | ||
this.setState({popups}) | ||
} | ||
@@ -212,4 +202,2 @@ } | ||
removePopupImmediately (id) { | ||
log('[popup]: closing ' + id) | ||
let popups = this.state.popups | ||
@@ -219,3 +207,3 @@ if (popups.has(id)) { | ||
this.setState({popups}) | ||
log('closed ' + id) | ||
return true | ||
@@ -254,7 +242,4 @@ } | ||
return ( | ||
<View | ||
pointerEvents={this.state.hasMask ? 'auto' : 'box-none'} | ||
style={[styles.full, {zIndex: 999}]} | ||
> | ||
{createPopupElements(this.state.popups)} | ||
<View pointerEvents='box-none' style={[styles.full, {zIndex: 999}]}> | ||
{createPopupElements(this.state.popups, this.props.maskColor)} | ||
</View> | ||
@@ -265,43 +250,3 @@ ) | ||
function log (info, isWarning) { | ||
// Dev flag is set in project config | ||
if (typeof __DEV__ !== 'undefined' && __DEV__) { | ||
if (isWarning) { | ||
console.warn('[popup]:', info) | ||
} else { | ||
console.log('[popup]:', info) | ||
} | ||
} | ||
} | ||
function hasMask (popups) { | ||
for (let popup of popups.values()) { | ||
if (popup.mask) { | ||
return true | ||
} | ||
} | ||
return false | ||
} | ||
function sortPopups (popups) { | ||
if (popups.size > 1) { | ||
// sort by zIndex | ||
return new Map([...popups.entries()].sort((a, b) => { | ||
return a[1].zIndex - b[1].zIndex | ||
})) | ||
} | ||
return popups | ||
} | ||
// TODO: reverse any valid keyframes definition | ||
function reverseKeyframes (keyframes) { | ||
return { | ||
from: keyframes.to, | ||
to: keyframes.from | ||
} | ||
} | ||
function createPopupElements (popups) { | ||
function createPopupElements (popups, maskColor) { | ||
log('render...size ' + popups.size) | ||
@@ -311,3 +256,3 @@ let popupElements = [] | ||
// Popups are independent to each other | ||
let pointerEvents = lockModeToPointerEvents(popup.lock) | ||
let pointerEvents = getPointerEvents(popup) | ||
popupElements.push( | ||
@@ -319,7 +264,7 @@ <View | ||
styles.full, | ||
getPositionStyle(popup.position) | ||
popup.position === 'center' ? styles.posCenter : null | ||
]} | ||
> | ||
{popup.mask ? <TouchableWithoutFeedback onPress={() => onAutoClose(popup)}> | ||
<View style={[styles.full, {backgroundColor: BG_TO}]} /> | ||
<View style={[styles.full, {backgroundColor: maskColor}]} /> | ||
</TouchableWithoutFeedback> : null} | ||
@@ -345,48 +290,18 @@ <Animatable.View | ||
function onAutoClose (popup) { | ||
if (popup && popup.lock === 'auto' && !popup._closing) { | ||
PopupStub.stub.removePopup(popup.id) | ||
if (popup && popup.autoClose && !popup._closing) { | ||
PopupStub.removePopup(popup.id) | ||
} | ||
} | ||
// lock mode is not exactly same as pointerEvents, | ||
// box-only and box-none will be removed in future. | ||
function lockModeToPointerEvents (mode) { | ||
switch (mode) { | ||
case 'auto': | ||
// if has a mask, enable autoclose | ||
// otherwise, blank erea is not responsible | ||
return 'auto' | ||
case 'all': | ||
case 'box-only': | ||
// all touches will be blocked | ||
return 'box-only' | ||
case 'box-none': | ||
case 'mask-only': | ||
// if has a mask, it won't autoclose | ||
// if not, blank erea may click through | ||
return 'box-none' | ||
case 'none': | ||
// touches will just pass through current popup | ||
return 'none' | ||
default: | ||
return 'auto' | ||
// map popup status to pointerEvents | ||
function getPointerEvents (popup) { | ||
if (popup._closing) { | ||
// all touches will be blocked, this will prevent double click | ||
return 'box-only' | ||
} | ||
} | ||
function getPositionStyle (pos) { | ||
switch (pos) { | ||
case 'bottom': | ||
return styles.posBottom | ||
case 'center': | ||
return styles.posCenter | ||
case 'left': | ||
return styles.posLeft | ||
case 'none': | ||
return null | ||
case 'right': | ||
return styles.posRight | ||
case 'top': | ||
return styles.posTop | ||
default: | ||
return styles.posCenter | ||
if (popup.mask) { | ||
return popup.autoClose ? 'auto' : 'box-none' | ||
} else { | ||
// only if there is no mask, blank erea can click through it's container | ||
return popup.enableClickThrough ? 'box-none' : 'auto' | ||
} | ||
@@ -403,20 +318,2 @@ } | ||
}, | ||
posLeft: { | ||
alignItems: 'center', | ||
justifyContent: 'flex-start' | ||
}, | ||
posRight: { | ||
alignItems: 'center', | ||
justifyContent: 'flex-end' | ||
}, | ||
posTop: { | ||
alignItems: 'center', | ||
flexDirection: 'column', | ||
justifyContent: 'flex-start' | ||
}, | ||
posBottom: { | ||
alignItems: 'center', | ||
flexDirection: 'column', | ||
justifyContent: 'flex-end' | ||
}, | ||
posCenter: { | ||
@@ -423,0 +320,0 @@ alignItems: 'center', |
@@ -5,4 +5,4 @@ # react-native-popup-stub | ||
[![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com) | ||
## Introduction | ||
## Introduction | ||
Popup global controller: | ||
@@ -18,3 +18,3 @@ | ||
## Installation | ||
## Installation | ||
``` | ||
@@ -26,22 +26,30 @@ npm install @unpourtous/react-native-popup-stub --save | ||
### Props | ||
PopupStub properties | ||
| param | type | description | | ||
| --- | --- | --- | | ||
| maskColor | String | mask color, default 'rgba(23,26,35,0.6)' | | ||
### PopupStub.init(_ref) | ||
Init PopupStub with PopupStub reference. | ||
| param | type | description | | ||
| param | type | description | | ||
| --- | --- | --- | | ||
| _ref | ref | should be the PopupStub component ref | | ||
### PopupStub.stub.addPopup(component, option) | ||
### PopupStub.addPopup(component, option) | ||
Add popup to PopupStub, use option to controller actions for each Component/Layers. | ||
| param | type | description | | ||
| param | type | description | | ||
| --- | --- | --- | | ||
| component | Component | View component | | ||
| option | Object | see below | | ||
| option.id | String | popup unique id, optional | | ||
| option.lock | Enum | popup layer lock mode, by default, 'auto' if has a mask, otherwise 'mask-only' | | ||
| option.mask | Boolean | has a mask or not, default true | | ||
| option.zIndex | Integer | same as in css, the priority of popup, the bigger the higher | | ||
| option.position | String | position of element in screen, available: none, left, right, top, bottom, center(defualt) | | ||
| option.wrapperStyle | Object | animation wrapper style (each popup is wrapped in an Animatable.View) | | ||
| .id | String | popup unique id, optional | | ||
| .mask | Boolean | has a visual mask or not, default true | | ||
| .autoClose | Boolean | enable clicking mask to close or not, default true | | ||
| .enableClickThrough | Boolean | blank erea (of container) may click through or not, default false | | ||
| .zIndex | Integer | same as in css, the priority of popup, the bigger the higher | | ||
| .position | String | position of element in screen, available: none, left, right, top, bottom, center(defualt) | | ||
| .wrapperStyle | Object | animation wrapper style (each popup is wrapped in an Animatable.View) | | ||
| Animatable.props | -- | see [Animatable.props](https://github.com/oblador/react-native-animatable), direction and onAnimationEnd are reserved | | ||
@@ -51,6 +59,8 @@ | ||
### PopupStub.stub.removePopup(id) | ||
**warning**: `lock` is deprecated from `v1.1.0` on, but still valid for a few versions. Use `autoClose` and `enableClickThrough` instead. | ||
### PopupStub.removePopup(id) | ||
Invoke popup exiting animation and remove it on animation end | ||
| param | type | description | | ||
| param | type | description | | ||
| --- | --- | --- | | ||
@@ -67,3 +77,3 @@ | id | String | popup unique id | ||
<View style={styles.container}> | ||
{/* Your root node */} | ||
{/* Your root node */} | ||
<TouchableHighlight | ||
@@ -77,4 +87,4 @@ onPress={() => { | ||
</TouchableHighlight> | ||
{/* Step One: Add popup stub */} | ||
{/* Step One: Add popup stub */} | ||
<PopupStub ref={_ref => { | ||
@@ -94,5 +104,5 @@ // Step Two: Init PopupStub itself | ||
static show (msg) { | ||
const id = PopupStub.stub.addPopup(<Toast msg={msg} />, { | ||
lock: 'none', | ||
const id = PopupStub.addPopup(<Toast msg={msg} />, { | ||
mask: false, | ||
enableClickThrough: true, | ||
position: 'center', | ||
@@ -107,6 +117,6 @@ zIndex: 500, | ||
setTimeout(() => { | ||
PopupStub.stub.removePopup(id) | ||
PopupStub.removePopup(id) | ||
}, 1500) | ||
} | ||
render () { | ||
@@ -122,8 +132,9 @@ return ( | ||
## TODO List | ||
## Todo | ||
- [x] Each popup an independent mask, rather than share a visual one | ||
- [ ] Support popup life circle callback or so | ||
- [ ] Enable mask animation | ||
- [ ] Enable remove animation in android | ||
- [ ] Enable reversing any valid animations | ||
- [ ] Support onAnimationEnd | ||
- [ ] Support onClose callback or so | ||
- [ ] Each popup an independent mask, rather than share a visual one | ||
@@ -130,0 +141,0 @@ ## License |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
219654
10
3756
137
1