Commit d0dbfc73 authored by 吴志俊's avatar 吴志俊

fix: modal内toggle失效

parent de072d17
...@@ -9,14 +9,43 @@ ...@@ -9,14 +9,43 @@
- ModalRouter: 始终只显示一个modal - ModalRouter: 始终只显示一个modal
- ModalRouter.Multi: 展示多个modal - ModalRouter.Multi: 展示多个modal
#### 内置toggle方法 ## Example
```javascript
import xx from './xx.jsx'
const map={x:xx}
<ModalRouter(.Multi) map={map} contextMode>
<div/>
<YourComponent/>
</ModalRouter(.Multi)>
//YourComponent
props.(...toggle)
const FuncTest = ({ children, ...rest }) => {
const { openModal, closeModal, closeAllModal } = useContext(AllModals)
//try to fire a timer to destroy this FC, make it looks like a Toast
useEffect(() => {
openModal('x')
return () => closeModal('x')
}, [])
return <span>FC Test</span>
}
```
## 内置toggle方法
- openModal(key,data) - openModal(key,data)
>map内key对应的组件会接收到modalData属性,如果data是个普通对象会自动展开 >map内key对应的组件会接收到modalData属性,如果data是个普通对象会自动展开
- closeModal(key?) - closeModal(key?)
>如果key不提供或者未匹配,默认pop
- closeAllModal() - closeAllModal()
## 配置属性 ### 配置属性
### map: { key: modal } ### map: { key: modal }
>key: 用于标识一个modal组件 >key: 用于标识一个modal组件
...@@ -28,10 +57,17 @@ ...@@ -28,10 +57,17 @@
> >
>默认ModalRouter所有儿子组件的props会接收到所有toogle方法 >默认ModalRouter所有儿子组件的props会接收到所有toogle方法
### container: ### container: function/class or object of React.ReactElement
>可传入自定义容器组件,记得处理props.children,以及接收并使用内置toggle方法 >可传入自定义容器组件,记得处理props.children,以及接收并使用内置toggle方法
>
>考虑配合内置导出对象 ScrollLocker,作用如下
### locker: HTMLElement
>暂且支持一个,用于解决scroll chain现象,阻止非Modal元素滚动
>
>默认为document.body
__默认container css:__ 默认 __container css:__
```css ```css
.HOC_modal { .HOC_modal {
position: fixed; position: fixed;
......
This diff is collapsed.
{ {
"name": "modal-router", "name": "modal-router",
"version": "0.0.4", "version": "0.0.7",
"description": "a simple way to toggle modals", "description": "a simple way to toggle modals",
"main": "dist/index.js", "main": "dist/index.js",
"scripts": { "scripts": {
"build": "webpack --config webpack.prod.config.js", "build": "webpack --config webpack.prod.config.js",
"dev": "webpack --config webpack.dev.config.js" "dev": "webpack serve --config webpack.dev.config.js"
}, },
"keywords": [ "keywords": [
"react", "react",
...@@ -23,12 +23,14 @@ ...@@ -23,12 +23,14 @@
"@babel/preset-react": "^7.12.10", "@babel/preset-react": "^7.12.10",
"babel-loader": "^8.2.2", "babel-loader": "^8.2.2",
"css-loader": "^5.0.1", "css-loader": "^5.0.1",
"html-webpack-plugin": "^4.5.1",
"react": "^17.0.1", "react": "^17.0.1",
"react-dom": "^17.0.1", "react-dom": "^17.0.1",
"style-loader": "^2.0.0", "style-loader": "^2.0.0",
"terser-webpack-plugin": "^5.1.1", "terser-webpack-plugin": "^5.1.1",
"webpack": "^5.15.0", "webpack": "^5.15.0",
"webpack-cli": "^4.3.1" "webpack-cli": "^4.3.1",
"webpack-dev-server": "^3.11.2"
}, },
"dependencies": {}, "dependencies": {},
"peerDependencies": { "peerDependencies": {
......
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="Web site created using create-react-app" />
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>
\ No newline at end of file
...@@ -6,8 +6,6 @@ import './index.css' ...@@ -6,8 +6,6 @@ import './index.css'
// issue: container 优化 不重新渲染 // issue: container 优化 不重新渲染
export const ModalCan = ({ container, hidden, children, ...funcs }) => { export const ModalCan = ({ container, hidden, children, ...funcs }) => {
const ref = useRef() const ref = useRef()
useListener(ref.current, 'mousewheel', NoPopAndDef)
useListener(ref.current, 'touchmove', NoPopAndDef)
// issue: 优先mount?HOC_modal // issue: 优先mount?HOC_modal
// if (hidden) { // if (hidden) {
......
...@@ -6,15 +6,8 @@ import { useUpdate, useModals } from "./hooks.jsx"; ...@@ -6,15 +6,8 @@ import { useUpdate, useModals } from "./hooks.jsx";
// todo: mutile type // todo: mutile type
export default function Multi ({ children, map, contextMode, container, ...rest }) { export default function Multi ({ children, map, contextMode, container, locker, ...rest }) {
const update = useUpdate() const [modals, modalData, { getModal, ...funcs }] = useModals(map, true, locker)
const [modals, modalData, { getModal, openModal: open, closeModal: close, closeAllModal: closeAll }] = useModals(map, true)
const openModal = useCallback(enhance(open, update), [])
const closeModal = useCallback(enhance(close, update), [])
const closeAllModal = useCallback(enhance(closeAll, update), [])
const funcs = { openModal, closeModal, closeAllModal }
const getChildren = enhanceChildren(children, funcs) const getChildren = enhanceChildren(children, funcs)
return ( return (
......
import { getScrollBarSize } from "./getScrollBarSize.jsx";
// issue: 多locker同时存在时,由于dom元素始终只有一个
// 对于同一个元素的所有操作都得考虑任意顺序的执行方法,结果的正确性
const keys = new WeakMap()
/**
* 用于锁定某滚动对象,全局公用一个存储池,由于dom元素即锁定对象唯一,在完全解锁前皆不可滚动
* @param element locker的dom元素,即锁定滚动的对象
*/
export const ScrollLocker = (element = document.body) => {
// check: valid Node
if (typeof element !== 'object' || !element?.baseURI || !document.body.contains(element)) {
return console.warn("ScrollLocker: get invalid Node or dom doesn't exit in document.body, locker can't work as expected")
}
let random = Math.random().toString(16).slice(2, 10)
let k
if (keys.has(element)) {
k = keys.get(element)
} else {
k = new key(element)
keys.set(element, k)
}
// 每个新locker的标识是random,统一存储在keys上
return {
lock: () => {
k.push(random)
},
unlock: () => {
k.pop(random)
},
unlockAll: () => {
k.clear(random)
}
}
}
/**
* 记录各元素锁定滚动状态
* element: 被锁定元素
* previousStyle: 记录样式
* locked/ed: 是否锁定
* count/countSum: 多个locker使用同一个key,锁定同一个dom,用于确定锁定状态,0时解锁
* locker:[{r,c}] r为生成的随机标识用于区分多个locker,c为单一locker计数
* push: 某locker在key上增次
* pop: 某locker在key上减次
* clear: 某locker在key上清零
*/
class key {
constructor(element) {
this.element = element
this.previousStyle = {
overflow: element.style.overflow,
width: element.style.width
}
this.ed = false
this.countSum = 0
// {r:标识,c:数目}
this.locker = []
}
push (r) {
let find = false
for (const lock of this.locker) {
if (lock.r === r) {
lock.c++
this.count++
find = true
}
}
if (!find) {
this.locker.push({ r, c: 1 })
this.count++
}
}
pop (r) {
for (const lock of this.locker) {
if (lock.r === r) {
if (lock.c >= 1) {
lock.c--
this.count--
}
}
}
}
clear (r) {
for (const lock of this.locker) {
if (lock.r === r) {
if (lock.c >= 1) {
this.count -= lock.c
lock.c = 0
}
}
}
}
// count->countSum->locked?
get count () { return this.countSum }
set count (n) {
if (n <= 0) {
n = 0
this.locked = false
} else this.locked = true
this.countSum = n
}
// locked?->style change
get locked () { return this.ed }
set locked (b) {
// issue:确定locker对象处于滚动状态
// if ((this.element === document.body && window.innerWidth > document.documentElement.clientWidth)
// || this.element.scrollHeight > this.element.clientHeight) {
let w = getScrollBarSize()
if (w) {
this.element.style.width = b ? `calc(100% - ${w}px)` : this.previousStyle.width
}
// }
this.element.style.overflow = b ? 'hidden' : this.previousStyle.overflow
this.ed = b
}
}
\ No newline at end of file
...@@ -6,15 +6,8 @@ import { useUpdate, useModals } from "./hooks.jsx"; ...@@ -6,15 +6,8 @@ import { useUpdate, useModals } from "./hooks.jsx";
// todos:lifetime\callback // todos:lifetime\callback
export default function Single ({ children, map, contextMode, container, ...rest }) { export default function Single ({ children, map, contextMode, container, locker, ...rest }) {
const update = useUpdate() const [modals, modalData, { getModal, ...funcs }] = useModals(map, false, locker)
const [modals, modalData, { getModal, openModal: open, closeModal: close, closeAllModal: closeAll }] = useModals(map, false)
const openModal = useCallback(enhance(open, update), [])
const closeModal = useCallback(enhance(close, update), [])
const closeAllModal = useCallback(enhance(closeAll, update), [])
const funcs = { openModal, closeModal, closeAllModal }
const getChildren = enhanceChildren(children, funcs) const getChildren = enhanceChildren(children, funcs)
return ( return (
......
...@@ -2,19 +2,23 @@ import React from "react"; ...@@ -2,19 +2,23 @@ import React from "react";
const name = 'ModalRouter: ' const name = 'ModalRouter: '
export const NotMatched = `${name}In the map you provided, key didn't matched` export const NotMatched = `${name}In the map you provided, key didn't matched`
export const NotMatchedButPop = `${name}In the stack you pushed, key didn't matched. Maybe you closeModal with the wrong name, this may causes bug.`
export const InvalidElement_i = i => `${name}The Index:${i} Child isn't valid React Element` export const InvalidElement_i = i => `${name}The Index:${i} Child isn't valid React Element`
export const InvalidElement = `${name}get InvalidElement from prop/child you given` export const InvalidElement = `${name}get InvalidElement from prop/child you given`
export const NoPopAndDef = (e) => { export const NoPopAndDef = (e) => {
e.preventDefault && e.preventDefault() e.preventDefault && e.preventDefault()
e.returnValue = false
e.stopPropagation && e.stopPropagation() e.stopPropagation && e.stopPropagation()
return false; e.returnValue = false
return false
} }
export const enhance = (mainFunc, sub) => { export const enhance = (mainFunc, ...subs) => {
return function () { return function () {
mainFunc(...arguments) mainFunc(...arguments)
sub() for (const sub of subs)
sub()
} }
} }
......
let stored
export const getScrollBarSize = (refresh) => {
if (refresh || !stored) {
let inner = document.createElement('div')
inner.style.height = '110%';
let outer = document.createElement('div')
outer.style.visibity = 'hidden'
outer.style.width = '100px'
outer.style.height = '100px'
outer.style.overflow = 'hidden'
outer.appendChild(inner)
document.body.appendChild(outer)
const hidden = inner.offsetWidth
outer.style.overflow = 'scroll'
let scroll = inner.offsetWidth
if (hidden === scroll) {
scroll = outer.clientWidth
}
stored = hidden - scroll
document.body.removeChild(outer)
}
return stored
}
\ No newline at end of file
import React, { Fragment, useCallback, useEffect, useMemo, useReducer, useRef, useState } from "react" import React, { Fragment, useCallback, useEffect, useMemo, useReducer, useRef, useState } from "react"
import { NotMatched } from "./constant.jsx"; import { NotMatched, NotMatchedButPop } from "./constant.jsx";
// 目的:打开modal,背景内容不滚动,modal本身和内部元素可滚动
// bug?关闭一个弹窗的同时打开另一个,页面会怎么样?
// 源自https://github.com/ant-design/ant-design/issues/19340
import { ScrollLocker } from "./ScrollLocker.jsx";
export const useUpdate = () => { export const useUpdate = () => {
const [, forceUpdate] = useReducer(o => [], null) const [, forceUpdate] = useReducer(o => [], null)
return forceUpdate return forceUpdate
} }
export const useListener = (element = window, event, handler, options = {}) => { // delete: element=window
// issue: ali的ahooks是默认window对象,
// 但是这里用到一个禁止滚动的func,对滚动的root不友好,删了
// 原因是ModalCan首次渲染时element先拿到undefined,会默认挂方法到window上,
// ahooks的没试过,不知道
export const useListener = (element, event, handler, options = {}) => {
let { current } = useRef() let { current } = useRef()
current = handler current = handler
useEffect(() => { useEffect(() => {
...@@ -22,9 +32,13 @@ export const useListener = (element = window, event, handler, options = {}) => { ...@@ -22,9 +32,13 @@ export const useListener = (element = window, event, handler, options = {}) => {
} }
// issue: necessery to useCallback? // issue: necessery to useCallback?
export const useModals = (map, multi) => { export const useModals = (map, multi, locker_) => {
// console.log('modals', Date.now());
const update = useUpdate()
const { current: modalData } = useRef([]) const { current: modalData } = useRef([])
const { current: modals } = useRef([]) const { current: modals } = useRef([])
// issue:始终锁定document.body
const { current: locker } = useRef(ScrollLocker(locker_))
// multi? always push or ? // multi? always push or ?
const open = useCallback((str, data) => { const open = useCallback((str, data) => {
...@@ -33,10 +47,15 @@ export const useModals = (map, multi) => { ...@@ -33,10 +47,15 @@ export const useModals = (map, multi) => {
modalData.push(data) modalData.push(data)
} else throw new ReferenceError(NotMatched) } else throw new ReferenceError(NotMatched)
}, [map]) }, [map])
const openModal = useCallback((str, data) => open(str, data), [map]) const openModal = useCallback((str, data) => {
open(str, data)
update()
locker.lock()
}, [map])
// multi? always splice or ? // multi? always splice or ?
// single: always close all? // single: always close all?
// bug!!!!!
// issue: same key/double open cause unmatch // issue: same key/double open cause unmatch
const close = useCallback(multi ? (str) => { const close = useCallback(multi ? (str) => {
let index = modals.findIndex((v, i) => v === str) let index = modals.findIndex((v, i) => v === str)
...@@ -44,18 +63,23 @@ export const useModals = (map, multi) => { ...@@ -44,18 +63,23 @@ export const useModals = (map, multi) => {
modals.splice(index, 1) modals.splice(index, 1)
modalData.splice(index, 1) modalData.splice(index, 1)
} else { } else {
console.warn(NotMatchedButPop)
modals.pop() modals.pop()
modalData.pop() modalData.pop()
} }
} : () => closeAllModal(), [multi]) } : () => closeAllModal(), [multi])
const closeModal = useCallback((str) => { const closeModal = useCallback((str) => {
close(str) close(str)
update()
locker.unlock()
}, []) }, [])
// issue: should this func always provided to developer // issue: should this func always provided to developer?
// especially in singleMode, remind them to close after open // especially in singleMode, remind them to close after open
const closeAllModal = useCallback(() => { const closeAllModal = useCallback(() => {
modals.length = 0 modals.length = 0
modalData.length = 0 modalData.length = 0
update()
locker.unlockAll()
}, []) }, [])
// 返回的是表达式还是函数? // 返回的是表达式还是函数?
......
...@@ -4,4 +4,8 @@ export { AllModals } from "./context.jsx"; ...@@ -4,4 +4,8 @@ export { AllModals } from "./context.jsx";
const ModalRouter = Single const ModalRouter = Single
ModalRouter.Multi = Multi ModalRouter.Multi = Multi
export default ModalRouter export default ModalRouter
\ No newline at end of file
// 返回工具
export { ScrollLocker } from './ScrollLocker.jsx'
// todo 返回数据队列,modals等,可能有用
\ No newline at end of file
import React from "react"
export function Control ({ openModal, closeModal, closeAllModal, who }) {
// console.log('control:', arguments)
const random = () => {
Math.random() > 0.5 ? openModal(who) : closeModal(who)
}
return <button className="button" onClick={random}>random{who}</button>
}
export function Open ({ openModal, who }) {
// console.log('open:', arguments)
return <button className="button" onClick={() => openModal(who)}>open{who}</button>
}
export function Close ({ closeModal, who }) {
// console.log('close:', arguments)
return <button className="button" onClick={() => closeModal(who)}>close{who}</button>
}
export const CloseAll = ({ closeAllModal }) => {
return <button className="button" onClick={() => { closeAllModal() }}>closeAll</button>
}
\ No newline at end of file
* {
margin: 0;
padding: 0;
}
body {
background-color: gray;
border: white 3px solid;
}
.simpleHead {
text-align: center;
color: black;
}
.simpleEle {
position: absolute;
bottom: 0;
left: 0;
right: 0;
margin: 0 auto;
text-align: center;
color: red;
}
.modal {
border: greenyellow 3px solid;
border-radius: 25px;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
color: red;
}
.small {
width: 35%;
height: 35%;
text-align: center;
vertical-align: middle;
background-color: black;
}
.big {
width: 75%;
height: 75%;
text-align: center;
vertical-align: middle;
background-color: black;
}
.giant {
width: 75%;
height: 200vh;
text-align: center;
vertical-align: middle;
background-color: black;
overflow: scroll;
}
.button {
width: 25%;
height: 50px;
margin: 3%;
border: black 3px solid;
border-radius: 5px;
color: blue;
}
import React, { Fragment, useContext, useEffect } from "react"
import ReactDOM from "react-dom"
import ModalRouter, { AllModals } from "../index.js";
import "./index.css";
import { SmallModal, BigModal, GiantModal, GiantContent } from "./modal.jsx";
import { Control, Open, Close } from "./control.jsx";
const map = {
small: SmallModal,
big: BigModal,
giant: <GiantModal><GiantContent /></GiantModal>,
}
const App = () => {
// console.log('app rendering...', Date.now());
return <Fragment>
<div className="simpleHead">simpleHead</div>
<ModalRouter.Multi map={map} contextMode>
<Open who='giant' />
<Open who='small' />
<Open who='big' />
<GiantModal />
<div className="simpleEle">
simpleEle
</div>
<div style={{ color: 'red', textAlign: 'center', width: '100%' }}>
<FuncTest />
</div>
</ModalRouter.Multi>
</Fragment>
}
const FuncTest = ({ children, ...rest }) => {
const { openModal, closeModal, closeAllModal } = useContext(AllModals)
useEffect(() => {
openModal('small')
return () => closeModal('small')
}, [])
return <span>FC Test</span>
}
ReactDOM.render(<App />, document.getElementById('root'))
\ No newline at end of file
import React from "react"
import { Open, Close, CloseAll } from "./control.jsx";
export const SmallModal = ({ children, openModal, closeModal, closeAllModal, ...rest }) => {
return <div className="modal small">
SmallModal{children}
<Close who='small' closeModal={closeModal} />
</div>
}
export const BigModal = ({ children, openModal, closeModal, closeAllModal, ...rest }) => {
return <div className="modal big">
BigModal{children}
<Open who='small' openModal={openModal} />
<Close who='big' closeModal={closeModal} />
</div>
}
export const GiantModal = ({ children, openModal, closeModal, closeAllModal, ...rest }) => {
return (
<div style={{ overflow: 'scroll' }}>
<div className="modal giant">
GiantModal
<Open who='small' openModal={openModal} />
<Close who='small' closeModal={closeModal} />
<Open who='big' openModal={openModal} />
<Close who='big' closeModal={closeModal} />
<Open who='giant' openModal={openModal} />
<Close who='giant' closeModal={closeModal} />
<CloseAll closeAllModal={closeAllModal} />
{children}
</div>
// </div>
)
}
export const GiantContent = ({ }) => {
return <div className="giant">
giantContent
</div>
}
\ No newline at end of file
const path = require('path') const path = require('path')
const TerserPlugin = require('terser-webpack-plugin') const html = require('html-webpack-plugin')
// test: npm link
// https://zhuanlan.zhihu.com/p/270649464
module.exports = { module.exports = {
mode: 'development', mode: 'development',
devtool: 'cheap-module-source-map',
entry: { entry: {
index: './src/index.js', index: './src/test/index.jsx',
}, },
output: { output: {
path: path.resolve(__dirname, 'dist'), path: path.resolve(__dirname, 'dist'),
filename: '[name].js', filename: '[name].js',
libraryTarget: 'umd', libraryTarget: 'umd',
}, },
// bug devServer: {
// https://caijialinxx.github.io/2019/11/20/fix-react-error-321-by-webpack-externals/ contentBase: path.join(__dirname + 'dist'),
externals: { hot: true,
react: { open: true,
commonjs: 'react',
commonjs2: 'react',
amd: 'react',
root: 'React',
},
'react-dom': {
commonjs: 'react-dom',
commonjs2: 'react-dom',
amd: 'react-dom',
root: 'ReactDOM',
},
}, },
module: { module: {
rules: [ rules: [
...@@ -51,6 +38,13 @@ module.exports = { ...@@ -51,6 +38,13 @@ module.exports = {
}, },
] ]
}, },
plugins: [
new html({
template: path.resolve(__dirname, './public/index.html'),
favicon: './public/favicon.ico',
hash: true,
})
],
optimization: { optimization: {
minimize: false, minimize: false,
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment