diff --git a/docs-vuepress/api/compile.md b/docs-vuepress/api/compile.md index f9eca8aee3..201c4fada2 100644 --- a/docs-vuepress/api/compile.md +++ b/docs-vuepress/api/compile.md @@ -88,7 +88,7 @@ mode 为 Mpx 编译的目标平台, 目前支持的有微信小程序(wx)\支 // 项目 package.json { "script": { - "build:cross": "mpx-cli-service build:mp --targets=wx,ali" + "build:cross": "mpx-cli-service build --targets=wx,ali" } } ``` diff --git a/docs-vuepress/guide/advance/platform.md b/docs-vuepress/guide/advance/platform.md index d7994f77a5..b1e7b7783c 100644 --- a/docs-vuepress/guide/advance/platform.md +++ b/docs-vuepress/guide/advance/platform.md @@ -65,7 +65,7 @@ new MpxwebpackPlugin({ // 项目 package.json { "script": { - "build:cross": "mpx-cli-service build:mp --targets=wx,ali" + "build:cross": "mpx-cli-service build --targets=wx,ali" } } ``` diff --git a/docs-vuepress/guide/basic/start.md b/docs-vuepress/guide/basic/start.md index 9812f5726b..541a2b72e9 100644 --- a/docs-vuepress/guide/basic/start.md +++ b/docs-vuepress/guide/basic/start.md @@ -5,6 +5,8 @@ npm i -g @mpxjs/cli ``` +> @mpxjs/cli文档 https://github.com/mpx-ecology/mpx-cli + ## 创建项目安装依赖 在当前目录下创建mpx项目。 @@ -29,7 +31,7 @@ npm install ## 编译构建 -使用npm script执行mpx的编译构建,在开发模式下我们执行watch命令,将项目源码构建输出到`dist/${平台目录}`下,并且监听源码的改动进行重新编译。 +使用npm script执行mpx的编译构建,在开发模式下我们执行serve命令,将项目源码构建输出到`dist/${平台目录}`下,并且监听源码的改动进行重新编译。 ```shell npm run serve diff --git a/docs-vuepress/guide/platform/index.md b/docs-vuepress/guide/platform/index.md index 7a2507b61a..290c3407c1 100644 --- a/docs-vuepress/guide/platform/index.md +++ b/docs-vuepress/guide/platform/index.md @@ -22,7 +22,7 @@ new MpxwebpackPlugin({ // 项目 package.json { "script": { - "build:cross": "mpx-cli-service build:mp --targets=wx,ali,ios,android" + "build:cross": "mpx-cli-service build --targets=wx,ali,ios,android" } } ``` diff --git a/docs-vuepress/guide/platform/rn.md b/docs-vuepress/guide/platform/rn.md index fe4285cd81..3363cb490c 100644 --- a/docs-vuepress/guide/platform/rn.md +++ b/docs-vuepress/guide/platform/rn.md @@ -1000,15 +1000,44 @@ text-shadow: 1rpx 3rpx 0 #2E0C02; | ---- | ---- | ---- | | entryPagePath | 支持 | 无| | pages | 支持 | 无 | +| window | 子属性部分支持 | 参考下面window配置部分 | | tabbar | 暂未支持 | 无 | | networkTimeout | 支持 | 无 | | subpackages | 支持 | 分包在RN下暂未进行拆包处理,仅能正常打包在一起,分包能力待后续支持 | | usingComponents | 支持 | | | vw | 支持 | 无 | -#### 路由能力 - +##### window配置 +app里面的window配置,参考[微信内window配置说明](https://developers.weixin.qq.com/miniprogram/dev/reference/configuration/app.html#window) +| 配置项 | 支持情况 | 特殊说明 | +| ---- | ---- | ---- | +| navigationBarBackgroundColor | 支持 | 无| +| navigationBarTextStyle | 支持 | 无 | +| navigationStyle | 支持 | 无 | +| backgroundColor | 支持 | 无 | +#### 页面配置 +页面配置内可配置页面级别的属性,参考[微信页面配置说明](https://developers.weixin.qq.com/miniprogram/dev/reference/configuration/page.html) +| 配置项 | 支持情况 | 特殊说明 | +| ---- | ---- | ---- | +| navigationBarBackgroundColor | 支持 | 无| +| navigationBarTextStyle | 支持 | 无 | +| navigationStyle | 支持 | 无 | +| backgroundColor | 支持 | 无 | +| usingComponents | 支持 | 无 | +| disableScroll | 不支持 | RN下默认页面不支持滚动,如需滚动需要使用可滚动的元素包裹 | + +#### 状态管理 +##### pinia +暂未支持 +##### store +已支持 +#### i18n +支持 +#### 原子类能力 +开发中,暂未支持 +#### 依赖注入(Provide/Inject) +开发中,暂未支持 ### 环境API diff --git a/examples/mpx-webview/H5/webviewbridge.min.js b/examples/mpx-webview/H5/webviewbridge.min.js index b94c23c965..7ac85a8e78 100644 --- a/examples/mpx-webview/H5/webviewbridge.min.js +++ b/examples/mpx-webview/H5/webviewbridge.min.js @@ -1,6 +1,6 @@ /** - * mpxjs webview bridge v2.9.53 + * mpxjs webview bridge v2.9.58 * (c) 2024 @mpxjs team * @license Apache */ -var e,t;e=this,t=function(){"use strict";function e(e,t,o){return(t=function(e){var t=function(e,t){if("object"!=typeof e||!e)return e;var o=e[Symbol.toPrimitive];if(void 0!==o){var n=o.call(e,t||"default");if("object"!=typeof n)return n;throw new TypeError("@@toPrimitive must return a primitive value.")}return("string"===t?String:Number)(e)}(e,"string");return"symbol"==typeof t?t:t+""}(t))in e?Object.defineProperty(e,t,{value:o,enumerable:!0,configurable:!0,writable:!0}):e[t]=o,e}function t(e,t){var o=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),o.push.apply(o,n)}return o}var o,n,a,r,i=function(o){for(var n=1;n-1&&g.indexOf("MiniProgram")>-1?c="my":g.toLowerCase().indexOf("miniprogram")>-1?c=g.indexOf("QQ")>-1?"qq":"wx":g.indexOf("swan/")>-1?c="swan":g.indexOf("toutiao")>-1?c="tt":(c="web",window.addEventListener("message",(function(e){var t=e.data,o=t;try{"string"==typeof t&&(o=JSON.parse(t))}catch(e){}var n=o,a=n.callbackId,r=n.error,i=n.result;void 0!==a&&d[a]&&(r?d[a](r):d[a](null,i),delete d[a])}),!1));var u=!1;function w(e){u?e():o.then((function(){u=!0,e()}))}var l={config:function(e){"wx"===c?w((function(){window.wx&&window.wx.config(e)})):console.warn("\u975e\u5fae\u4fe1\u73af\u5883\u4e0d\u9700\u8981\u914d\u7f6econfig")}};function f(e){if("[object Object]"!==Object.prototype.toString.call(e))return e;var t={};for(var o in e)"function"!=typeof e[o]&&(t[o]=e[o]);return t}function m(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};if("getEnv"!==e){var o=++s;d[o]=function(e,n){e?(t.fail&&t.fail(e),t.complete&&t.complete(e)):(t.success&&t.success(n),t.complete&&t.complete(n)),delete d[o]};var n={type:e,callbackId:s,payload:f(t)};void 0!==p&&(n.clientUid=p),window.ReactNativeWebView?window.ReactNativeWebView.postMessage&&window.ReactNativeWebView.postMessage(JSON.stringify(n)):window.parent.postMessage&&window.parent.postMessage(n,"*")}else t({webapp:!0})}var v=function(){var e={wx:{keyName:"miniProgram",api:["navigateTo","navigateBack","switchTab","reLaunch","redirectTo","postMessage","getEnv"]},tt:{keyName:"miniProgram",api:["redirectTo","navigateTo","switchTab","reLaunch","navigateBack","setSwipeBackModeSync","postMessage","getEnv","checkJsApi","chooseImage","compressImage","previewImage","uploadFile","getNetworkType","openLocation","getLocation"]},swan:{keyName:"webView",api:["navigateTo","navigateBack","switchTab","reLaunch","redirectTo","getEnv","postMessage"]},qq:{keyName:"miniProgram",api:["navigateTo","navigateBack","switchTab","reLaunch","redirectTo","getEnv","postMessage"]}}[c]||{},t={wx:["checkJSApi","chooseImage","previewImage","uploadImage","downloadImage","getLocalImgData","startRecord","stopRecord","onVoiceRecordEnd","playVoice","pauseVoice","stopVoice","onVoicePlayEnd","uploadVoice","downloadVoice","translateVoice","getNetworkType","openLocation","getLocation","startSearchBeacons","stopSearchBeacons","onSearchBeacons","scanQRCode","chooseCard","addCard","openCard"],my:["navigateTo","navigateBack","switchTab","reLaunch","redirectTo","chooseImage","previewImage","getLocation","openLocation","alert","showLoading","hideLoading","getNetworkType","startShare","tradePay","postMessage","onMessage","getEnv"],swan:["makePhoneCall","setClipboardData","getNetworkType","openLocation","getLocation","chooseLocation","chooseImage","previewImage","openShare","navigateToSmartProgram"],web:["navigateTo","navigateBack","switchTab","reLaunch","redirectTo","getEnv","postMessage","getLoadError","getLocation"],tt:[]}[c]||[];(e.api||[]).forEach((function(t){l[t]=function(){for(var o=arguments.length,n=new Array(o),a=0;a1&&void 0!==arguments[1]?arguments[1]:{},o=t.time,n=void 0===o?5e3:o,a=t.crossOrigin,r=void 0!==a&&a;function i(){return new Promise((function(t,o){var n=document.createElement("script");n.type="text/javascript",n.async="async",r&&(n.crossOrigin="anonymous"),n.onload=n.onreadystatechange=function(){this.readyState&&!/^(loaded|complete)$/.test(this.readyState)||(t(),n.onload=n.onreadystatechange=null)},n.onerror=function(){o(new Error("load ".concat(e," error"))),n.onerror=null},n.src=e,document.getElementsByTagName("head")[0].appendChild(n)}))}function c(){return new Promise((function(t,o){setTimeout((function(){o(new Error("load ".concat(e," timeout")))}),n)}))}return Promise.race([i(),c()])}(i[c].url):Promise.reject(new Error("\u672a\u627e\u5230\u5bf9\u5e94\u7684sdk")):Promise.resolve(),v(),l},"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e=e||self).mpx=t(); \ No newline at end of file +var e,o;e=this,o=function(){"use strict";var e,o,a,t,n=Object.assign({wx:{url:"https://res.wx.qq.com/open/js/jweixin-1.3.2.js"},qq:{url:"https://qqq.gtimg.cn/miniprogram/webview_jssdk/qqjssdk-1.0.0.js"},my:{url:"https://appx/web-view.min.js"},swan:{url:"https://b.bdstatic.com/searchbox/icms/searchbox/js/swan-2.0.4.js"},tt:{url:"https://lf3-cdn-tos.bytegoofy.com/obj/goofy/developer/jssdk/jssdk-1.2.1.js"}},window.sdkUrlMap),i=null,r=0,c=(a=location.href,(t=/mpx_webview_id=(\d+)/g.exec(a))&&t[1]&&(o=+t[1]),o),s={},d=navigator.userAgent;d.indexOf("AlipayClient")>-1&&d.indexOf("MiniProgram")>-1?i="my":d.toLowerCase().indexOf("miniprogram")>-1?i=d.indexOf("QQ")>-1?"qq":"wx":d.indexOf("swan/")>-1?i="swan":d.indexOf("toutiao")>-1?i="tt":(i="web",window.addEventListener("message",(function(e){var o=e.data,a=o;try{"string"==typeof o&&(a=JSON.parse(o))}catch(e){}var t=a,n=t.callbackId,i=t.error,r=t.result;void 0!==n&&s[n]&&(i?s[n](i):s[n](null,r),delete s[n])}),!1));var g=!1;function w(o){g?o():e.then((function(){g=!0,o()}))}var p={config:function(e){"wx"===i?w((function(){window.wx&&window.wx.config(e)})):console.warn("\u975e\u5fae\u4fe1\u73af\u5883\u4e0d\u9700\u8981\u914d\u7f6econfig")}};function l(e){for(var o=arguments.length,a=new Array(o>1?o-1:0),t=1;t1&&void 0!==arguments[1]?arguments[1]:{},a=o.time,t=void 0===a?5e3:a,n=o.crossOrigin,i=void 0!==n&&n;function r(){return new Promise((function(o,a){var t=document.createElement("script");t.type="text/javascript",t.async="async",i&&(t.crossOrigin="anonymous"),t.onload=t.onreadystatechange=function(){this.readyState&&!/^(loaded|complete)$/.test(this.readyState)||(o(),t.onload=t.onreadystatechange=null)},t.onerror=function(){a(new Error("load ".concat(e," error"))),t.onerror=null},t.src=e,document.getElementsByTagName("head")[0].appendChild(t)}))}function c(){return new Promise((function(o,a){setTimeout((function(){a(new Error("load ".concat(e," timeout")))}),t)}))}return Promise.race([r(),c()])}(n[i].url):Promise.reject(new Error("\u672a\u627e\u5230\u5bf9\u5e94\u7684sdk")):Promise.resolve(),m(),p},"object"==typeof exports&&"undefined"!=typeof module?module.exports=o():"function"==typeof define&&define.amd?define(o):(e=e||self).mpx=o(); \ No newline at end of file diff --git a/packages/api-proxy/@types/index.d.ts b/packages/api-proxy/@types/index.d.ts index b3f325b2c8..733c2cb306 100644 --- a/packages/api-proxy/@types/index.d.ts +++ b/packages/api-proxy/@types/index.d.ts @@ -110,6 +110,19 @@ export const createVideoContext: WechatMiniprogram.Wx['createVideoContext'] export const onWindowResize: WechatMiniprogram.Wx['onWindowResize'] export const offWindowResize: WechatMiniprogram.Wx['offWindowResize'] export const createAnimation: WechatMiniprogram.Wx['createAnimation'] +export const hideHomeButton: WechatMiniprogram.Wx['hideHomeButton'] +export const getSetting: WechatMiniprogram.Wx['getSetting'] +export const openSetting: WechatMiniprogram.Wx['openSetting'] +export const enableAlertBeforeUnload: WechatMiniprogram.Wx['enableAlertBeforeUnload'] +export const disableAlertBeforeUnload: WechatMiniprogram.Wx['disableAlertBeforeUnload'] +export const getMenuButtonBoundingClientRect: WechatMiniprogram.Wx['getMenuButtonBoundingClientRect'] +export const getImageInfo: WechatMiniprogram.Wx['getImageInfo'] +export const vibrateShort: WechatMiniprogram.Wx['vibrateShort'] +export const vibrateLong: WechatMiniprogram.Wx['vibrateLong'] +export const getExtConfig: WechatMiniprogram.Wx['getExtConfig'] +export const getExtConfigSync: WechatMiniprogram.Wx['getExtConfigSync'] +export const openLocation: WechatMiniprogram.Wx['openLocation'] +export const chooseLocation: WechatMiniprogram.Wx['chooseLocation'] declare const install: (...args: any) => any diff --git a/packages/api-proxy/src/platform/api/action-sheet/rnActionSheet.jsx b/packages/api-proxy/src/platform/api/action-sheet/rnActionSheet.jsx index 4e68c19bca..e28b212dcf 100644 --- a/packages/api-proxy/src/platform/api/action-sheet/rnActionSheet.jsx +++ b/packages/api-proxy/src/platform/api/action-sheet/rnActionSheet.jsx @@ -1,38 +1,17 @@ -import { View, TouchableHighlight, Text, StyleSheet, Button, Animated } from 'react-native' +import { View, TouchableHighlight, Text, StyleSheet, TouchableOpacity } from 'react-native' import { successHandle, failHandle } from '../../../common/js' import { Portal } from '@ant-design/react-native' +import { getWindowInfo } from '../system/rnSystem' +import Animated, { + useSharedValue, + useAnimatedStyle, + withTiming +} from 'react-native-reanimated' function showActionSheet (options = {}) { const { alertText, itemList = [], itemColor = '#000000', success, fail, complete } = options - let actionSheetKey - const slideAnim = new Animated.Value(500) - const slideIn = () => { - // Will change fadeAnim value to 1 in 5 seconds - Animated.timing(slideAnim, { - toValue: 0, - duration: 200, - useNativeDriver: true, - }).start() - } - const slideOut = () => { - // Will change fadeAnim value to 1 in 5 seconds - Animated.timing(slideAnim, { - toValue: 500, - duration: 200, - useNativeDriver: true, - }).start(() => { - }) - } - if (itemList.length === 0 || itemList.length > 6) { - const result = { - errMsg: 'showActionSheet:fail parameter error: itemList should not be large than 6' - } - if (itemList.length === 0) { - result.errno = 1001 - result.errMsg = 'showActionSheet:fail parameter error: parameter.itemList should have at least 1 item;' - } - failHandle(result, fail, complete) - return - } + const windowInfo = getWindowInfo() + const bottom = windowInfo.screenHeight - windowInfo.safeArea.bottom + let actionSheetKey = null const styles = StyleSheet.create({ actionActionMask: { left: 0, @@ -44,16 +23,14 @@ function showActionSheet (options = {}) { zIndex: 1000 }, actionSheetContent: { - left: 0, - right: 0, - position: 'absolute', - bottom: 0, backgroundColor: '#ffffff', borderTopLeftRadius: 10, borderTopRightRadius: 10, - transform: [{ - translateY: -500 - }] + position: 'absolute', + bottom: 0, + left: 0, + right: 0, + paddingBottom: bottom }, itemStyle: { paddingTop: 15, @@ -73,53 +50,62 @@ function showActionSheet (options = {}) { paddingBottom: 10 } }) - const remove = function () { - if (actionSheetKey) { - slideOut() - setTimeout(() => { - Portal.remove(actionSheetKey) - actionSheetKey = null - }, 200) + function ActionSheet () { + const offset = useSharedValue(1000); + + const animatedStyles = useAnimatedStyle(() => ({ + transform: [{ translateY: offset.value }], + })) + + const slideOut = () => { + // Will change fadeAnim value to 1 in 5 seconds + offset.value = withTiming(1000) } - } - const selectAction = function (index) { - const result = { - errMsg: 'showActionSheet:ok', - tapIndex: index + + offset.value = withTiming(0) + + const selectAction = function (index) { + const result = { + errMsg: 'showActionSheet:ok', + tapIndex: index + } + successHandle(result, success, complete) + remove() } - successHandle(result, success, complete) - remove() - } - const cancelAction = function () { - const result = { - errMsg: 'showActionSheet:fail cancel' + + const remove = function () { + if (actionSheetKey) { + slideOut() + setTimeout(() => { + Portal.remove(actionSheetKey) + actionSheetKey = null + }, 200) + } } - failHandle(result, fail, complete) - remove() - } - let alertTextList = [] - if (alertText) { - alertTextList = [alertText] + + const cancelAction = function () { + const result = { + errMsg: 'showActionSheet:fail cancel' + } + failHandle(result, fail, complete) + remove() + } + return ( + + + { alertText ? {alertText} : null } + { itemList.map((item, index) => selectAction(index)} style={ [styles.itemStyle, itemList.length -1 === index ? { + borderBottomWidth: 6, + borderBottomStyle: 'solid', + borderBottomColor: '#f7f7f7' + } : {}] }>{item}) } + 取消 + + + ) } - const ActionSheetView = - - { alertTextList.map((item, index) => {item}) } - { itemList.map((item, index) => selectAction(index)} style={ [styles.itemStyle, itemList.length -1 === index ? { - borderBottomWidth: 6, - borderBottomStyle: 'solid', - borderBottomColor: '#f7f7f7' - } : {}] }>{item}) } - - - - actionSheetKey = Portal.add(ActionSheetView) - slideIn() + + actionSheetKey = Portal.add() } export { diff --git a/packages/api-proxy/src/platform/api/app/index.web.js b/packages/api-proxy/src/platform/api/app/index.web.js index 2be7f32689..571fa27485 100644 --- a/packages/api-proxy/src/platform/api/app/index.web.js +++ b/packages/api-proxy/src/platform/api/app/index.web.js @@ -3,8 +3,27 @@ import { isBrowser, isReact } from '@mpxjs/utils' global.__mpxAppCbs = global.__mpxAppCbs || { show: [], hide: [], - error: [] + error: [], + rejection: [] +} + +function off (cbs, cb) { + if (cb) { + const idx = cbs.indexOf(cb) + if (idx > -1) cbs.splice(idx, 1) + } else { + cbs.length = 0 + } +} + +function onUnhandledRejection (callback) { + if (isBrowser || isReact) { + global.__mpxAppCbs.rejection.push(callback) + } +} +function offUnhandledRejection (callback) { + off(global.__mpxAppCbs.rejection, callback) } function onError (callback) { @@ -14,9 +33,7 @@ function onError (callback) { } function offError (callback) { - const cbs = global.__mpxAppCbs.error - const index = cbs.indexOf(callback) - if (index > -1) cbs.splice(index, 1) + off(global.__mpxAppCbs.error, callback) } function onAppShow (callback) { @@ -26,9 +43,7 @@ function onAppShow (callback) { } function offAppShow (callback) { - const cbs = global.__mpxAppCbs.show - const index = cbs.indexOf(callback) - if (index > -1) cbs.splice(index, 1) + off(global.__mpxAppCbs.show, callback) } function onAppHide (callback) { @@ -38,9 +53,7 @@ function onAppHide (callback) { } function offAppHide (callback) { - const cbs = global.__mpxAppCbs.hide - const index = cbs.indexOf(callback) - if (index > -1) cbs.splice(index, 1) + off(global.__mpxAppCbs.hide, callback) } export { @@ -49,5 +62,7 @@ export { offAppShow, offAppHide, onError, - offError + offError, + onUnhandledRejection, + offUnhandledRejection } diff --git a/packages/api-proxy/src/platform/api/create-intersection-observer/rnIntersectionObserver.js b/packages/api-proxy/src/platform/api/create-intersection-observer/rnIntersectionObserver.js index 059ae453b0..038b67099b 100644 --- a/packages/api-proxy/src/platform/api/create-intersection-observer/rnIntersectionObserver.js +++ b/packages/api-proxy/src/platform/api/create-intersection-observer/rnIntersectionObserver.js @@ -96,7 +96,7 @@ class RNIntersectionObserver { const windowRect = { top: navigationLayout.y + this.margins.top, - left: navigationLayout.x + this.margins.left, + left: this.margins.left, right: navigationLayout.width - this.margins.right, bottom: navigationLayout.y + navigationLayout.height - this.margins.bottom } diff --git a/packages/api-proxy/src/platform/api/image/index.ali.js b/packages/api-proxy/src/platform/api/image/index.ali.js index 1c24c8c2b2..4c64e5d4d9 100644 --- a/packages/api-proxy/src/platform/api/image/index.ali.js +++ b/packages/api-proxy/src/platform/api/image/index.ali.js @@ -30,7 +30,10 @@ function compressImage (options = {}) { return ENV_OBJ.compressImage(opts) } +const getImageInfo = ENV_OBJ.getImageInfo + export { previewImage, - compressImage + compressImage, + getImageInfo } diff --git a/packages/api-proxy/src/platform/api/image/index.ios.js b/packages/api-proxy/src/platform/api/image/index.ios.js new file mode 100644 index 0000000000..4ed1ca50fa --- /dev/null +++ b/packages/api-proxy/src/platform/api/image/index.ios.js @@ -0,0 +1,45 @@ +import { envError, defineUnsupportedProps, successHandle, failHandle } from '../../../common/js' +import { Image } from 'react-native' + +const previewImage = envError('previewImage') + +const compressImage = envError('compressImage') + +const getImageInfo = function (options = {}) { + const { src, success, fail, complete } = options + if (src === undefined) { + const result = { + errMsg: 'getImageInfo:fail parameter error: parameter.src should be String instead of Undefined;', + errno: 1001 + } + failHandle(result, fail, complete) + return + } + if (src === '') { + const result = { + errMsg: 'getImageInfo:fail image not found' + } + failHandle(result, fail, complete) + return + } + Image.getSize(src, (width, height) => { + const result = { + errMsg: 'getImageInfo:ok', + width, + height + } + defineUnsupportedProps(result, ['path', 'orientation', 'type']) + successHandle(result, success, complete) + }, (err) => { + const result = { + errMsg: 'getImageInfo:fail download image fail. reason: ' + err + } + failHandle(result, fail, complete) + }) +} + +export { + previewImage, + compressImage, + getImageInfo +} diff --git a/packages/api-proxy/src/platform/api/image/index.js b/packages/api-proxy/src/platform/api/image/index.js index 2bdd9a104e..5a94edd980 100644 --- a/packages/api-proxy/src/platform/api/image/index.js +++ b/packages/api-proxy/src/platform/api/image/index.js @@ -4,7 +4,10 @@ const previewImage = ENV_OBJ.previewImage || envError('previewImage') const compressImage = ENV_OBJ.compressImage || envError('compressImage') +const getImageInfo = ENV_OBJ.getImageInfo || envError('getImageInfo') + export { previewImage, - compressImage + compressImage, + getImageInfo } diff --git a/packages/api-proxy/src/platform/api/image/index.web.js b/packages/api-proxy/src/platform/api/image/index.web.js index a8fea9b762..19bbc0489f 100644 --- a/packages/api-proxy/src/platform/api/image/index.web.js +++ b/packages/api-proxy/src/platform/api/image/index.web.js @@ -1,5 +1,5 @@ import Preview from './Preview' -import { isBrowser, throwSSRWarning, envError } from '../../../common/js' +import { isBrowser, throwSSRWarning, envError, defineUnsupportedProps, successHandle, failHandle } from '../../../common/js' let preview = null @@ -15,9 +15,53 @@ const previewImage = (options) => { if (!preview) preview = new Preview() preview.show(options) } + const compressImage = envError('compressImage') +const getImageInfo = function (options = {}) { + const { src, success, fail, complete } = options + + if (src === undefined) { + const result = { + errMsg: 'getImageInfo:fail parameter error: parameter.src should be String instead of Undefined;', + errno: 1001 + } + failHandle(result, fail, complete) + return + } + if (src === '') { + const result = { + errMsg: 'getImageInfo:fail image not found' + } + failHandle(result, fail, complete) + return + } + + const img = new Image() + img.src = src + + img.onload = function () { + const width = img.width + const height = img.height + const result = { + errMsg: 'getImageInfo:ok', + width, + height + } + defineUnsupportedProps(result, ['path', 'orientation', 'type']) + successHandle(result, success, complete) + } + + img.onerror = function () { + const result = { + errMsg: 'getImageInfo:fail download image fail. ' + } + failHandle(result, fail, complete) + } +} + export { previewImage, - compressImage + compressImage, + getImageInfo } diff --git a/packages/api-proxy/src/platform/api/modal/rnModal.jsx b/packages/api-proxy/src/platform/api/modal/rnModal.jsx index 11060afae0..afc8ec1251 100644 --- a/packages/api-proxy/src/platform/api/modal/rnModal.jsx +++ b/packages/api-proxy/src/platform/api/modal/rnModal.jsx @@ -17,7 +17,7 @@ const showModal = function (options = {}) { fail, complete } = options - const modalWidth = width - 60 + const modalWidth = width * 0.8 const styles = StyleSheet.create({ modalTask: { left: 0, @@ -53,7 +53,8 @@ const showModal = function (options = {}) { lineHeight: 26, color: '#808080', paddingLeft: 20, - paddingRight: 20 + paddingRight: 20, + textAlign: 'center' }, modalBtnBox: { borderTopWidth: StyleSheet.hairlineWidth, @@ -67,8 +68,8 @@ const showModal = function (options = {}) { modalBtn: { flex: 1, textAlign: 'center', - paddingTop: 10, - paddingBottom: 10, + paddingTop: width * 0.04, + paddingBottom: width * 0.04, }, modalButton: { width: '100%', @@ -88,9 +89,8 @@ const showModal = function (options = {}) { let editableContent = [] let modalButton = [{ text: confirmText, - confirmColor, type: 'confirm', - color: 'rgb(87, 107, 149)' + color: confirmColor }] let contentText = content const onChangeText = function (text) { @@ -128,10 +128,9 @@ const showModal = function (options = {}) { if (showCancel) { modalButton.unshift({ text: cancelText, - cancelColor, type: 'cancel', style: styles.cancelStyle, - color: '#000000' + color: cancelColor }) } ModalView = diff --git a/packages/api-proxy/src/platform/api/set-navigation-bar/index.ali.js b/packages/api-proxy/src/platform/api/set-navigation-bar/index.ali.js index 67ebfd1f5c..a71a31e5bf 100644 --- a/packages/api-proxy/src/platform/api/set-navigation-bar/index.ali.js +++ b/packages/api-proxy/src/platform/api/set-navigation-bar/index.ali.js @@ -17,7 +17,12 @@ function setNavigationBarColor (options = {}) { return ENV_OBJ.setNavigationBar(options) } +function hideHomeButton (options = {}) { + return ENV_OBJ.hideBackHome(options) +} + export { setNavigationBarTitle, - setNavigationBarColor + setNavigationBarColor, + hideHomeButton } diff --git a/packages/api-proxy/src/platform/api/set-navigation-bar/index.ios.js b/packages/api-proxy/src/platform/api/set-navigation-bar/index.ios.js index 1171cd7b68..b3fd0834e8 100644 --- a/packages/api-proxy/src/platform/api/set-navigation-bar/index.ios.js +++ b/packages/api-proxy/src/platform/api/set-navigation-bar/index.ios.js @@ -1,4 +1,4 @@ -import { successHandle, failHandle, getFocusedNavigation } from '../../../common/js' +import { successHandle, failHandle, getFocusedNavigation, envError } from '../../../common/js' import { nextTick } from '../next-tick' function setNavigationBarTitle (options = {}) { const { title = '', success, fail, complete } = options @@ -31,7 +31,10 @@ function setNavigationBarColor (options = {}) { } } +const hideHomeButton = envError('hideHomeButton') + export { setNavigationBarTitle, - setNavigationBarColor + setNavigationBarColor, + hideHomeButton } diff --git a/packages/api-proxy/src/platform/api/set-navigation-bar/index.js b/packages/api-proxy/src/platform/api/set-navigation-bar/index.js index dcfc49d1e1..a13f7acc26 100644 --- a/packages/api-proxy/src/platform/api/set-navigation-bar/index.js +++ b/packages/api-proxy/src/platform/api/set-navigation-bar/index.js @@ -4,7 +4,10 @@ const setNavigationBarTitle = ENV_OBJ.setNavigationBarTitle || envError('setNavi const setNavigationBarColor = ENV_OBJ.setNavigationBarColor || envError('setNavigationBarColor') +const hideHomeButton = ENV_OBJ.hideHomeButton || envError('hideHomeButton') + export { setNavigationBarTitle, - setNavigationBarColor + setNavigationBarColor, + hideHomeButton } diff --git a/packages/api-proxy/src/platform/api/set-navigation-bar/index.web.js b/packages/api-proxy/src/platform/api/set-navigation-bar/index.web.js index 487912e1d4..198ac4b945 100644 --- a/packages/api-proxy/src/platform/api/set-navigation-bar/index.web.js +++ b/packages/api-proxy/src/platform/api/set-navigation-bar/index.web.js @@ -1,4 +1,4 @@ -import { isBrowser, throwSSRWarning, successHandle } from '../../../common/js' +import { isBrowser, envError, throwSSRWarning, successHandle } from '../../../common/js' function setNavigationBarTitle (options = {}) { if (!isBrowser) { @@ -26,7 +26,10 @@ function setNavigationBarColor (options = {}) { successHandle({ errMsg: 'setNavigationBarColor:ok' }, success, complete) } +const hideHomeButton = envError('hideHomeButton') + export { setNavigationBarTitle, - setNavigationBarColor + setNavigationBarColor, + hideHomeButton } diff --git a/packages/api-proxy/src/platform/api/setting/index.js b/packages/api-proxy/src/platform/api/setting/index.js new file mode 100644 index 0000000000..5ab739177f --- /dev/null +++ b/packages/api-proxy/src/platform/api/setting/index.js @@ -0,0 +1,19 @@ +import { ENV_OBJ, envError } from '../../../common/js' + +const getSetting = ENV_OBJ.getSetting || envError('getSetting') + +const openSetting = ENV_OBJ.openSetting || envError('openSetting') + +const enableAlertBeforeUnload = ENV_OBJ.enableAlertBeforeUnload || envError('enableAlertBeforeUnload') + +const disableAlertBeforeUnload = ENV_OBJ.disableAlertBeforeUnload || envError('disableAlertBeforeUnload') + +const getMenuButtonBoundingClientRect = ENV_OBJ.getMenuButtonBoundingClientRect || envError('getMenuButtonBoundingClientRect') + +export { + getSetting, + openSetting, + enableAlertBeforeUnload, + disableAlertBeforeUnload, + getMenuButtonBoundingClientRect +} diff --git a/packages/api-proxy/src/platform/api/system/rnSystem.js b/packages/api-proxy/src/platform/api/system/rnSystem.js index 2284971c9b..acf3965ed9 100644 --- a/packages/api-proxy/src/platform/api/system/rnSystem.js +++ b/packages/api-proxy/src/platform/api/system/rnSystem.js @@ -10,8 +10,8 @@ const getWindowInfo = function () { const insets = Object.assign(initialWindowMetricsInset, navigationInsets) let safeArea = {} const { top = 0, bottom = 0, left = 0, right = 0 } = insets - const screenHeight = dimensionsScreen.height - const screenWidth = dimensionsScreen.width + const screenHeight = __mpx_mode__ === 'ios' ? dimensionsScreen.height : dimensionsScreen.height - bottom // 解决安卓开启屏幕内三建导航安卓把安全区计算进去后产生的影响 + const screenWidth = __mpx_mode__ === 'ios' ? dimensionsScreen.width : dimensionsScreen.width - right const layout = navigation.layout || {} const layoutHeight = layout.height || 0 const layoutWidth = layout.width || 0 diff --git a/packages/api-proxy/src/platform/api/toast/rnToast.jsx b/packages/api-proxy/src/platform/api/toast/rnToast.jsx index e780ffb556..45f98fa9e6 100644 --- a/packages/api-proxy/src/platform/api/toast/rnToast.jsx +++ b/packages/api-proxy/src/platform/api/toast/rnToast.jsx @@ -14,8 +14,8 @@ const styles = StyleSheet.create({ backgroundColor: 'rgba(20, 20, 20, 0.7)', paddingTop: 15, paddingBottom: 15, - paddingLeft: 10, - paddingRight: 10, + paddingLeft: 20, + paddingRight: 20, borderRadius: 5, display: 'flex', flexDirection: 'column', diff --git a/packages/api-proxy/src/platform/index.js b/packages/api-proxy/src/platform/index.js index dda41337d6..67f2d93a10 100644 --- a/packages/api-proxy/src/platform/index.js +++ b/packages/api-proxy/src/platform/index.js @@ -43,7 +43,7 @@ export * from './api/file' // getUserInfo export * from './api/get-user-info' -// previewImage, compressImage +// previewImage, compressImage, getImageInfo export * from './api/image' // login @@ -116,3 +116,6 @@ export * from './api/vibrate' // onKeyboardHeightChange, offKeyboardHeightChange, hideKeyboard export * from './api/keyboard' + +// getSetting, openSetting, enableAlertBeforeUnload, disableAlertBeforeUnload, getMenuButtonBoundingClientRect +export * from './api/setting' diff --git a/packages/core/package.json b/packages/core/package.json index eee049452c..5e0a073d0d 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -32,6 +32,7 @@ "@react-navigation/stack": "^7.0.4", "react": "*", "react-native": "*", + "promise": "^8.3.0", "react-native-gesture-handler": "^2.19.0", "react-native-linear-gradient": "^2.8.3", "react-native-safe-area-context": "^4.14.0", @@ -61,6 +62,9 @@ "react-native": { "optional": true }, + "promise": { + "optional": true + }, "@react-navigation/native": { "optional": true }, @@ -106,4 +110,4 @@ }, "sideEffects": false, "gitHead": "2d37697869b9bdda3efab92dda8c910b68fd05c0" -} +} \ No newline at end of file diff --git a/packages/core/src/core/proxy.js b/packages/core/src/core/proxy.js index bbbe68759a..54d4603f55 100644 --- a/packages/core/src/core/proxy.js +++ b/packages/core/src/core/proxy.js @@ -51,7 +51,7 @@ import { } from './innerLifecycle' import contextMap from '../dynamic/vnode/context' import { getAst } from '../dynamic/astCache' -import { inject, provide } from '../platform/export/apiInject' +import { inject, provide } from '../platform/export/inject' let uid = 0 diff --git a/packages/core/src/external/vue.js b/packages/core/src/external/vue.js deleted file mode 100644 index b1c6ea436a..0000000000 --- a/packages/core/src/external/vue.js +++ /dev/null @@ -1 +0,0 @@ -export default {} diff --git a/packages/core/src/external/vue.web.js b/packages/core/src/external/vue.web.js deleted file mode 100644 index 1ff878300e..0000000000 --- a/packages/core/src/external/vue.web.js +++ /dev/null @@ -1,6 +0,0 @@ -import Vue from 'vue' -import install from './vuePlugin' - -Vue.use(install) - -export default Vue diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 1ee8bcef3b..95fbcea821 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -1,8 +1,7 @@ -import Vue from './external/vue' + import { error, diffAndCloneA, hasOwn, makeMap } from '@mpxjs/utils' import { APIs, InstanceAPIs } from './platform/export/api' - -import { createI18n } from './platform/builtInMixins/i18nMixin' +import { init } from './platform/env/index' export * from './platform/export/index' @@ -123,10 +122,6 @@ function factory () { Object.assign(Mpx, APIs) Object.assign(Mpx.prototype, InstanceAPIs) - // 输出web时在mpx上挂载Vue对象 - if (__mpx_mode__ === 'web') { - Mpx.__vue = Vue - } return Mpx } @@ -156,12 +151,6 @@ Mpx.config = { rnConfig: {} } -global.__mpx = Mpx - -if (__mpx_mode__ !== 'web') { - if (global.i18n) { - Mpx.i18n = createI18n(global.i18n) - } -} +init(Mpx) export default Mpx diff --git a/packages/core/src/platform/builtInMixins/directiveHelperMixin.ios.js b/packages/core/src/platform/builtInMixins/directiveHelperMixin.ios.js index 2183bd03d0..d5ced280f9 100644 --- a/packages/core/src/platform/builtInMixins/directiveHelperMixin.ios.js +++ b/packages/core/src/platform/builtInMixins/directiveHelperMixin.ios.js @@ -1,7 +1,10 @@ export default function directiveHelperMixin () { return { methods: { - __getWxKey (item, key) { + __getWxKey (item, key, index) { + if (key === 'index') { + return index + } return key === '*this' ? item : item[key] } } diff --git a/packages/core/src/platform/builtInMixins/styleHelperMixin.ios.js b/packages/core/src/platform/builtInMixins/styleHelperMixin.ios.js index 28fbe08a4a..3566007f75 100644 --- a/packages/core/src/platform/builtInMixins/styleHelperMixin.ios.js +++ b/packages/core/src/platform/builtInMixins/styleHelperMixin.ios.js @@ -181,7 +181,7 @@ export default function styleHelperMixin () { } else if (appClassMap[className]) { // todo 全局样式在每个页面和组件中生效,以支持全局原子类,后续支持样式模块复用后可考虑移除 Object.assign(result, appClassMap[className]) - } else if (this.__props[className] && isObject(this.__props[className])) { + } else if (isObject(this.__props[className])) { // externalClasses必定以对象形式传递下来 Object.assign(result, this.__props[className]) } diff --git a/packages/core/src/platform/createApp.ios.js b/packages/core/src/platform/createApp.ios.js index 0d7f98aa60..b3ef722394 100644 --- a/packages/core/src/platform/createApp.ios.js +++ b/packages/core/src/platform/createApp.ios.js @@ -1,6 +1,6 @@ import transferOptions from '../core/transferOptions' import builtInKeysMap from './patch/builtInKeysMap' -import { makeMap, spreadProp, parseUrlQuery, getFocusedNavigation, hasOwn, extend } from '@mpxjs/utils' +import { makeMap, spreadProp, getFocusedNavigation, hasOwn, extend } from '@mpxjs/utils' import { mergeLifecycle } from '../convertor/mergeLifecycle' import { LIFECYCLE } from '../platform/patch/lifecycle/index' import Mpx from '../index' @@ -81,12 +81,6 @@ export default function createApp (option, config = {}) { } } - global.__mpxAppCbs = global.__mpxAppCbs || { - show: [], - hide: [], - error: [] - } - global.__mpxAppLaunched = false global.__mpxAppFocusedState = ref('show') @@ -102,14 +96,10 @@ export default function createApp (option, config = {}) { }) if (!global.__mpxAppLaunched) { - const parsed = Mpx.config.rnConfig.parseAppProps?.(props) || {} - if (parsed.url) { - const { path, queryObj } = parseUrlQuery(parsed.url) - Object.assign(initialRouteRef.current, { - initialRouteName: path.startsWith('/') ? path.slice(1) : path, - initialParams: queryObj - }) - } + const { initialRouteName, initialParams } = Mpx.config.rnConfig.parseAppProps?.(props) || {} + initialRouteRef.current.initialRouteName = initialRouteName || initialRouteRef.current.initialRouteName + initialRouteRef.current.initialParams = initialParams || initialRouteRef.current.initialParams + global.__mpxAppOnLaunch = (navigation) => { global.__mpxAppLaunched = true const state = navigation.getState() @@ -137,6 +127,9 @@ export default function createApp (option, config = {}) { if (defaultOptions.onError) { global.__mpxAppCbs.error.push(defaultOptions.onError.bind(instance)) } + if (defaultOptions.onUnhandledRejection) { + global.__mpxAppCbs.rejection.push(defaultOptions.onUnhandledRejection.bind(instance)) + } const changeSubscription = ReactNative.AppState.addEventListener('change', (currentState) => { if (currentState === 'active') { @@ -159,7 +152,7 @@ export default function createApp (option, config = {}) { if (navigation && hasOwn(global.__mpxPageStatusMap, navigation.pageId)) { global.__mpxPageStatusMap[navigation.pageId] = 'show' } - } else if (currentState === 'inactive') { + } else if (currentState === 'inactive' || currentState === 'background') { global.__mpxAppCbs.hide.forEach((cb) => { cb() }) @@ -190,11 +183,11 @@ export default function createApp (option, config = {}) { const { initialRouteName, initialParams } = initialRouteRef.current const headerBackImageProps = Mpx.config.rnConfig.headerBackImageProps || null const navScreenOpts = { - gestureEnabled: true, // 7.x替换headerBackTitleVisible // headerBackButtonDisplayMode: 'minimal', headerBackTitleVisible: false, - headerMode: 'float' + // 安卓上会出现初始化时闪现导航条的问题 + headerShown: false } if (headerBackImageProps) { navScreenOpts.headerBackImage = () => { diff --git a/packages/core/src/platform/createApp.js b/packages/core/src/platform/createApp.js index b22bab336d..0f35cf97e5 100644 --- a/packages/core/src/platform/createApp.js +++ b/packages/core/src/platform/createApp.js @@ -5,7 +5,7 @@ import { makeMap, spreadProp, isBrowser } from '@mpxjs/utils' import { mergeLifecycle } from '../convertor/mergeLifecycle' import { LIFECYCLE } from '../platform/patch/lifecycle/index' import Mpx from '../index' -import { initAppProvides } from './export/apiInject' +import { initAppProvides } from './export/inject' const appHooksMap = makeMap(mergeLifecycle(LIFECYCLE).app) @@ -47,11 +47,6 @@ export default function createApp (option, config = {}) { } global.__mpxEnterOptions = options this.$options.onLaunch && this.$options.onLaunch.call(this, options) - global.__mpxAppCbs = global.__mpxAppCbs || { - show: [], - hide: [], - error: [] - } if (isBrowser) { if (this.$options.onShow) { this.$options.onShow.call(this, options) @@ -63,6 +58,9 @@ export default function createApp (option, config = {}) { if (this.$options.onError) { global.__mpxAppCbs.error.push(this.$options.onError.bind(this)) } + if (this.$options.onUnhandledRejection) { + global.__mpxAppCbs.rejection.push(this.$options.onUnhandledRejection.bind(this)) + } } } }) diff --git a/packages/core/src/platform/env/event.js b/packages/core/src/platform/env/event.js new file mode 100644 index 0000000000..82cb7bf77a --- /dev/null +++ b/packages/core/src/platform/env/event.js @@ -0,0 +1,108 @@ +import { isBrowser } from '@mpxjs/utils' + +function extendEvent (e, extendObj = {}) { + Object.keys(extendObj).forEach((key) => { + Object.defineProperty(e, key, { + value: extendObj[key], + enumerable: true, + configurable: true, + writable: true + }) + }) +} + +function MpxEvent (layer) { + this.targetElement = null + this.touches = [] + this.touchStartX = 0 + this.touchStartY = 0 + this.startTimer = null + this.needTap = true + this.isTouchDevice = document && ('ontouchstart' in document.documentElement) + + this.onTouchStart = (event) => { + if (event.targetTouches?.length > 1) { + return true + } + this.touches = event.targetTouches + this.targetElement = event.target + this.needTap = true + this.startTimer = null + this.touchStartX = this.touches[0].pageX + this.touchStartY = this.touches[0].pageY + this.startTimer = setTimeout(() => { + this.needTap = false + this.sendEvent(this.targetElement, 'longpress', event) + this.sendEvent(this.targetElement, 'longtap', event) + }, 350) + } + + this.onTouchMove = (event) => { + const touch = event.changedTouches[0] + if (Math.abs(touch.pageX - this.touchStartX) > 1 || Math.abs(touch.pageY - this.touchStartY) > 1) { + this.needTap = false + this.startTimer && clearTimeout(this.startTimer) + this.startTimer = null + } + } + + this.onTouchEnd = (event) => { + if (event.targetTouches?.length > 1) { + return true + } + this.startTimer && clearTimeout(this.startTimer) + this.startTimer = null + if (this.needTap) { + this.sendEvent(this.targetElement, 'tap', event) + } + } + + this.onClick = (event) => { + this.targetElement = event.target + this.sendEvent(this.targetElement, 'tap', event) + } + this.sendEvent = (targetElement, type, event) => { + const touchEvent = new CustomEvent(type, { + bubbles: true, + cancelable: true + }) + const changedTouches = event.changedTouches || [] + extendEvent(touchEvent, { + timeStamp: event.timeStamp, + changedTouches, + touches: changedTouches, + detail: { + // pc端点击事件可能没有changedTouches,所以直接从 event中取 + x: changedTouches[0]?.pageX || event.pageX || 0, + y: changedTouches[0]?.pageY || event.pageY || 0 + } + }) + targetElement && targetElement.dispatchEvent(touchEvent) + } + + this.addListener = () => { + if (this.isTouchDevice) { + layer.addEventListener('touchstart', this.onTouchStart, true) + layer.addEventListener('touchmove', this.onTouchMove, true) + layer.addEventListener('touchend', this.onTouchEnd, true) + } else { + layer.addEventListener('click', this.onClick, true) + } + } + this.addListener() +} + +export function initEvent () { + if (isBrowser && !global.__mpxCreatedEvent) { + global.__mpxCreatedEvent = true + if (document.readyState === 'complete' || document.readyState === 'interactive') { + // eslint-disable-next-line no-new + new MpxEvent(document.body) + } else { + document.addEventListener('DOMContentLoaded', function () { + // eslint-disable-next-line no-new + new MpxEvent(document.body) + }, false) + } + } +} diff --git a/packages/core/src/platform/env/index.ios.js b/packages/core/src/platform/env/index.ios.js new file mode 100644 index 0000000000..f18fcbcf4c --- /dev/null +++ b/packages/core/src/platform/env/index.ios.js @@ -0,0 +1,51 @@ +import { createI18n } from '../builtInMixins/i18nMixin' + +export function init (Mpx) { + global.__mpx = Mpx + global.__mpxAppCbs = global.__mpxAppCbs || { + show: [], + hide: [], + error: [], + rejection: [] + } + if (global.i18n) { + Mpx.i18n = createI18n(global.i18n) + } + initGlobalErrorHandling() +} + +function initGlobalErrorHandling () { + if (global.ErrorUtils) { + const defaultHandler = global.ErrorUtils.getGlobalHandler() + global.ErrorUtils.setGlobalHandler((error, isFatal) => { + if (global.__mpxAppCbs && global.__mpxAppCbs.error && global.__mpxAppCbs.error.length) { + global.__mpxAppCbs.error.forEach((cb) => { + cb(error) + }) + } else if (defaultHandler) { + defaultHandler(error, isFatal) + } else { + console.error(`${error.name}: ${error.message}\n`) + } + }) + } + + const rejectionTrackingOptions = { + allRejections: true, + onUnhandled (id, error) { + if (global.__mpxAppCbs && global.__mpxAppCbs.rejection && global.__mpxAppCbs.rejection.length) { + global.__mpxAppCbs.rejection.forEach((cb) => { + cb(error, id) + }) + } else { + console.warn(`UNHANDLED PROMISE REJECTION (id: ${id}): ${error}\n`) + } + } + } + + if (global?.HermesInternal?.hasPromise?.()) { + global.HermesInternal?.enablePromiseRejectionTracker?.(rejectionTrackingOptions) + } else { + require('promise/setimmediate/rejection-tracking').enable(rejectionTrackingOptions) + } +} diff --git a/packages/core/src/platform/env/index.js b/packages/core/src/platform/env/index.js new file mode 100644 index 0000000000..3382486e17 --- /dev/null +++ b/packages/core/src/platform/env/index.js @@ -0,0 +1,8 @@ +import { createI18n } from '../builtInMixins/i18nMixin' + +export function init (Mpx) { + global.__mpx = Mpx + if (global.i18n) { + Mpx.i18n = createI18n(global.i18n) + } +} diff --git a/packages/core/src/platform/env/index.web.js b/packages/core/src/platform/env/index.web.js new file mode 100644 index 0000000000..58dccf2d03 --- /dev/null +++ b/packages/core/src/platform/env/index.web.js @@ -0,0 +1,48 @@ +import Vue from 'vue' +import install from './vuePlugin' +import { isBrowser, error, warn } from '@mpxjs/utils' +import { initEvent } from './event' + +export function init (Mpx) { + global.__mpx = Mpx + global.__mpxAppCbs = global.__mpxAppCbs || { + show: [], + hide: [], + error: [], + rejection: [] + } + Mpx.__vue = Vue + Vue.use(install) + initEvent() + initGlobalErrorHandling() +} + +function initGlobalErrorHandling () { + Vue.config.errorHandler = (e, vm, info) => { + error(`Unhandled error occurs${info ? ` during execution of [${info}]` : ''}!`, vm?.__mpxProxy?.options.mpxFileResource, e) + } + Vue.config.warnHandler = (msg, vm, trace) => { + warn(msg, vm?.__mpxProxy?.options.mpxFileResource, trace) + } + + if (isBrowser) { + window.addEventListener('error', (event) => { + if (global.__mpxAppCbs && global.__mpxAppCbs.error && global.__mpxAppCbs.error.length) { + global.__mpxAppCbs.error.forEach((cb) => { + cb(event.error) + }) + } else { + console.error(`${event.type}: ${event.message}\n`) + } + }) + window.addEventListener('unhandledrejection', (event) => { + if (global.__mpxAppCbs && global.__mpxAppCbs.rejection && global.__mpxAppCbs.rejection.length) { + global.__mpxAppCbs.rejection.forEach((cb) => { + cb(event.reason, event.promise) + }) + } else { + console.warn(`UNHANDLED PROMISE REJECTION: ${event.reason}\n`) + } + }) + } +} diff --git a/packages/core/src/external/vuePlugin.js b/packages/core/src/platform/env/vuePlugin.js similarity index 98% rename from packages/core/src/external/vuePlugin.js rename to packages/core/src/platform/env/vuePlugin.js index f83b9f0734..4ed365d986 100644 --- a/packages/core/src/external/vuePlugin.js +++ b/packages/core/src/platform/env/vuePlugin.js @@ -1,7 +1,7 @@ import { walkChildren, parseSelector, error, hasOwn, collectDataset } from '@mpxjs/utils' import { createSelectorQuery, createIntersectionObserver } from '@mpxjs/api-proxy' import { EffectScope } from 'vue' -import { PausedState } from '../helper/const' +import { PausedState } from '../../helper/const' const hackEffectScope = () => { EffectScope.prototype.pause = function () { diff --git a/packages/core/src/platform/export/index.js b/packages/core/src/platform/export/index.js index 75b59133bb..bf570a7994 100644 --- a/packages/core/src/platform/export/index.js +++ b/packages/core/src/platform/export/index.js @@ -46,4 +46,4 @@ export { export { provide, inject -} from './apiInject' +} from './inject' diff --git a/packages/core/src/platform/export/apiInject.js b/packages/core/src/platform/export/inject.js similarity index 100% rename from packages/core/src/platform/export/apiInject.js rename to packages/core/src/platform/export/inject.js diff --git a/packages/core/src/platform/export/apiInject.web.js b/packages/core/src/platform/export/inject.web.js similarity index 100% rename from packages/core/src/platform/export/apiInject.web.js rename to packages/core/src/platform/export/inject.web.js diff --git a/packages/core/src/platform/patch/getDefaultOptions.ios.js b/packages/core/src/platform/patch/getDefaultOptions.ios.js index a794fe4160..ac4c5c7561 100644 --- a/packages/core/src/platform/patch/getDefaultOptions.ios.js +++ b/packages/core/src/platform/patch/getDefaultOptions.ios.js @@ -3,7 +3,7 @@ import * as ReactNative from 'react-native' import { ReactiveEffect } from '../../observer/effect' import { watch } from '../../observer/watch' import { reactive, set, del } from '../../observer/reactive' -import { hasOwn, isFunction, noop, isObject, isArray, getByPath, collectDataset, hump2dash, wrapMethodsWithErrorHandling } from '@mpxjs/utils' +import { hasOwn, isFunction, noop, isObject, isArray, getByPath, collectDataset, hump2dash, dash2hump, callWithErrorHandling, wrapMethodsWithErrorHandling } from '@mpxjs/utils' import MpxProxy from '../../core/proxy' import { BEFOREUPDATE, ONLOAD, UPDATED, ONSHOW, ONHIDE, ONRESIZE, REACTHOOKSEXEC } from '../../core/innerLifecycle' import mergeOptions from '../../core/mergeOptions' @@ -52,20 +52,18 @@ function createEffect (proxy, components) { proxy.effect = new ReactiveEffect(() => { // reset instance proxy.target.__resetInstance() - return proxy.target.__injectedRender(innerCreateElement, getComponent) + return callWithErrorHandling(proxy.target.__injectedRender.bind(proxy.target), proxy, 'render function', [innerCreateElement, getComponent]) }, () => queueJob(update), proxy.scope) // render effect允许自触发 proxy.toggleRecurse(true) } -function getRootProps (props) { +function getRootProps (props, validProps) { const rootProps = {} for (const key in props) { - if (hasOwn(props, key)) { - const match = /^(bind|catch|capture-bind|capture-catch|style|enable-var):?(.*?)(?:\.(.*))?$/.exec(key) - if (match) { - rootProps[key] = props[key] - } + const altKey = dash2hump(key) + if (!hasOwn(validProps, key) && !hasOwn(validProps, altKey) && key !== 'children') { + rootProps[key] = props[key] } } return rootProps @@ -474,7 +472,8 @@ export function getDefaultOptions ({ type, rawOptions = {}, currentInject }) { const root = useMemo(() => proxy.effect.run(), [finalMemoVersion]) if (root && root.props.ishost) { - const rootProps = getRootProps(props) + // 对于组件未注册的属性继承到host节点上,如事件、样式和其他属性等 + const rootProps = getRootProps(props, validProps) rootProps.style = Object.assign({}, root.props.style, rootProps.style) // update root props return cloneElement(root, rootProps) @@ -560,10 +559,7 @@ export function getDefaultOptions ({ type, rawOptions = {}, currentInject }) { backgroundColor: pageConfig.backgroundColor || '#ffffff' }, ref: rootRef, - onLayout, - onTouchStart: () => { - ReactNative.Keyboard.isVisible() && ReactNative.Keyboard.dismiss() - } + onLayout }, createElement(RouteContext.Provider, { diff --git a/packages/utils/src/errorHandling.js b/packages/utils/src/errorHandling.js index 29ded07c64..442705cbfc 100644 --- a/packages/utils/src/errorHandling.js +++ b/packages/utils/src/errorHandling.js @@ -2,7 +2,7 @@ import { isFunction, isPromise } from './base' import { error } from './log' function handleError (e, instance, info) { - error(`Unhandled error occurs${info ? ` during execution of [${info}]` : ''}!`, instance?.options?.mpxFileResource, e) + error(`Unhandled error occurs${info ? ` during execution of [${info}]` : ''}!`, instance?.options.mpxFileResource, e) } export function callWithErrorHandling (fn, instance, info, args) { @@ -21,7 +21,7 @@ export function callWithErrorHandling (fn, instance, info, args) { } export function wrapMethodsWithErrorHandling (methods, instance) { - if (process.env.NODE_ENV === 'production') return methods + // if (process.env.NODE_ENV === 'production') return methods const newMethods = {} Object.keys(methods).forEach((key) => { if (isFunction(methods[key])) { diff --git a/packages/utils/src/log.js b/packages/utils/src/log.js index eba7143ff3..72fb7aed90 100644 --- a/packages/utils/src/log.js +++ b/packages/utils/src/log.js @@ -18,8 +18,9 @@ export function warn (msg, location, e) { const warnHandler = global.__mpx?.config.warnHandler if (isFunction(warnHandler)) { warnHandler(msg, location, e) + } else { + log('warn', msg, location, e) } - return log('warn', msg, location, e) } } @@ -27,8 +28,9 @@ export function error (msg, location, e) { const errorHandler = global.__mpx?.config.errorHandler if (isFunction(errorHandler)) { errorHandler(msg, location, e) + } else { + log('error', msg, location, e) } - return log('error', msg, location, e) } function log (type, msg, location, e) { diff --git a/packages/webpack-plugin/lib/platform/style/wx/index.js b/packages/webpack-plugin/lib/platform/style/wx/index.js index f654b35481..8a8cc97e3d 100644 --- a/packages/webpack-plugin/lib/platform/style/wx/index.js +++ b/packages/webpack-plugin/lib/platform/style/wx/index.js @@ -373,7 +373,8 @@ module.exports = function getSpec ({ warn, error }) { // transform 转换 const formatTransform = ({ prop, value, selector }, { mode }) => { - if (Array.isArray(value)) return { prop, value } + // css var & 数组直接返回 + if (Array.isArray(value) || calcExp.test(value)) return { prop, value } const values = parseValues(value) const transform = [] values.forEach(item => { @@ -398,7 +399,7 @@ module.exports = function getSpec ({ warn, error }) { break case 'matrix': case 'matrix3d': - transform.push({ [key]: val.split(',').map(val => +val) }) + transform.push({ [key]: parseValues(val, ',').map(val => +val) }) break case 'translate': case 'scale': @@ -409,7 +410,7 @@ module.exports = function getSpec ({ warn, error }) { { // 2 个以上的值处理 key = key.replace('3d', '') - const vals = val.split(',', key === 'rotate' ? 4 : 3) + const vals = parseValues(val, ',').splice(0, key === 'rotate' ? 4 : 3) // scale(.5) === scaleX(.5) scaleY(.5) if (vals.length === 1 && key === 'scale') { vals.push(vals[0]) @@ -455,14 +456,17 @@ module.exports = function getSpec ({ warn, error }) { const formatFlex = ({ prop, value, selector }) => { let values = parseValues(value) + // 值大于3 去前三 if (values.length > 3) { - error(`Value of [flex] in ${selector} supports up to three values, received [${value}], please check again!`) + warn(`Value of [flex] in ${selector} supports up to three values, received [${value}], please check again!`) values = values.splice(0, 3) } const cssMap = [] - const lastOne = values[values.length - 1] - const isAuto = lastOne === 'auto' - // 枚举值 none initial + // 单个css var 直接设置 flex 属性 + if (values.length === 1 && cssVariableExp.test(value)) { + return { prop, value } + } + // 包含枚举值 none initial if (values.includes('initial') || values.includes('none')) { // css flex: initial ===> flex: 0 1 ===> rn flex 0 1 // css flex: none ===> css flex: 0 0 ===> rn flex 0 0 @@ -475,38 +479,31 @@ module.exports = function getSpec ({ warn, error }) { } return cssMap } - // 最后一个值是flexBasis 的有效值(auto或者有单位百分比、px等) - // flex 0 1 auto flex auto flex 1 auto flex 1 30px flex 1 10% flex 1 1 auto - if (!isNumber(lastOne) || !cssVariableExp.test(value)) { - // 添加 grow 和 shrink - // 在设置 flex basis 有效值的场景下,如果没有设置 grow 和 shrink,则默认为1 - // 单值 flex: 1 1 - // 双值 flex: 1 - // 三值 flex: - for (let i = 0; i < 2; i++) { - const item = getIntegersFlex({ prop: AbbreviationMap[prop][i], value: isNumber(values[i]) || cssVariableExp.test(value) ? values[i] : 1 }) + // 只有1-2个值且最后的值是flexBasis 的有效值(auto或者有单位百分比、px等) + // 在设置 flex basis 有效值的场景下,如果没有设置 grow 和 shrink,则默认为1 + // 单值 flex: 1 1 + // 双值 flex: 1 + // 三值 flex: + for (let i = 0; i < 3; i++) { + if (i < 2) { + // 添加 grow 和 shrink + const isValid = isNumber(values[0]) || cssVariableExp.test(values[0]) + // 兜底 1 + const val = isValid ? values[0] : 1 + const item = getIntegersFlex({ prop: AbbreviationMap[prop][i], value: val, selector }) item && cssMap.push(item) + isValid && values.shift() + } else { + // 添加 flexBasis + // 有单位(百分比、px等) 的 value 赋值 flexBasis,auto 不处理,兜底 0 + const val = values[0] || 0 + if (val !== 'auto') { + cssMap.push({ + prop: 'flexBasis', + value: val + }) + } } - if (!isAuto) { - // 有单位(百分比、px等) 的 value 赋值 flexBasis,auto 不处理 - cssMap.push({ - prop: 'flexBasis', - value: lastOne - }) - } - return cssMap - } - // 纯数值:value 按flex-grow flex-shrink flex-basis 顺序赋值 - // 兜底 shrink & basis - if (values.length === 1) { - values.push(...[1, 0]) - } else if (values.length === 2) { - values.push(0) - } - // 循环赋值 - for (let i = 0; i < values.length; i++) { - const item = getIntegersFlex({ prop: AbbreviationMap[prop][i], value: values[i] }) - item && cssMap.push(item) } return cssMap } @@ -514,7 +511,7 @@ module.exports = function getSpec ({ warn, error }) { const formatFontFamily = ({ prop, value, selector }) => { // 去掉引号 取逗号分隔后的第一个 const newVal = value.replace(/"|'/g, '').trim() - const values = newVal.split(',').filter(i => i) + const values = parseValues(newVal, ',') if (!newVal || !values.length) { error(`Value of [${prop}] is invalid in ${selector}, received [${value}], please check again!`) return false diff --git a/packages/webpack-plugin/lib/platform/template/wx/component-config/rich-text.js b/packages/webpack-plugin/lib/platform/template/wx/component-config/rich-text.js index f1372e600c..0fe38178a2 100644 --- a/packages/webpack-plugin/lib/platform/template/wx/component-config/rich-text.js +++ b/packages/webpack-plugin/lib/platform/template/wx/component-config/rich-text.js @@ -12,6 +12,14 @@ module.exports = function ({ print }) { el.isBuiltIn = true return 'mpx-rich-text' }, + ios (tag, { el }) { + el.isBuiltIn = true + return 'mpx-rich-text' + }, + android (tag, { el }) { + el.isBuiltIn = true + return 'mpx-rich-text' + }, props: [ { test: /^(space)$/, diff --git a/packages/webpack-plugin/lib/platform/template/wx/component-config/unsupported.js b/packages/webpack-plugin/lib/platform/template/wx/component-config/unsupported.js index afe24bb48a..e02ab6e331 100644 --- a/packages/webpack-plugin/lib/platform/template/wx/component-config/unsupported.js +++ b/packages/webpack-plugin/lib/platform/template/wx/component-config/unsupported.js @@ -11,7 +11,7 @@ const JD_UNSUPPORTED_TAG_NAME_ARR = ['functional-page-navigator', 'live-pusher', // 快应用不支持的标签集合 const QA_UNSUPPORTED_TAG_NAME_ARR = ['movable-view', 'movable-area', 'open-data', 'official-account', 'editor', 'functional-page-navigator', 'live-player', 'live-pusher', 'ad', 'cover-image'] // RN不支持的标签集合 -const RN_UNSUPPORTED_TAG_NAME_ARR = ['open-data', 'official-account', 'editor', 'functional-page-navigator', 'live-player', 'live-pusher', 'ad', 'progress', 'rich-text', 'slider', 'audio', 'camera', 'video', 'match-media', 'page-container', 'editor', 'keyboard-accessory', 'map'] +const RN_UNSUPPORTED_TAG_NAME_ARR = ['open-data', 'official-account', 'editor', 'functional-page-navigator', 'live-player', 'live-pusher', 'ad', 'progress', 'slider', 'audio', 'camera', 'video', 'match-media', 'page-container', 'editor', 'keyboard-accessory', 'map'] /** * @param {function(object): function} print diff --git a/packages/webpack-plugin/lib/runtime/components/react/event.config.ts b/packages/webpack-plugin/lib/runtime/components/react/event.config.ts index b3f4633ad2..b1356b7ed9 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/event.config.ts +++ b/packages/webpack-plugin/lib/runtime/components/react/event.config.ts @@ -2,31 +2,30 @@ interface EventConfig { [key: string]: string[]; } -const eventConfigMap: EventConfig = { - bindtap: ['onTouchStart', 'onTouchMove', 'onTouchEnd'], - bindlongpress: ['onTouchStart', 'onTouchMove', 'onTouchEnd', 'onTouchCancel'], - bindtouchstart: ['onTouchStart'], - bindtouchmove: ['onTouchMove'], - bindtouchend: ['onTouchEnd'], - bindtouchcancel: ['onTouchCancel'], - catchtap: ['onTouchStart', 'onTouchMove', 'onTouchEnd'], - catchlongpress: ['onTouchStart', 'onTouchMove', 'onTouchEnd', 'onTouchCancel'], - catchtouchstart: ['onTouchStart'], - catchtouchmove: ['onTouchMove'], - catchtouchend: ['onTouchEnd'], - catchtouchcancel: ['onTouchCancel'], - 'capture-bindtap': ['onTouchStartCapture', 'onTouchMoveCapture', 'onTouchEndCapture'], - 'capture-bindlongpress': ['onTouchStartCapture', 'onTouchMoveCapture', 'onTouchEndCapture', 'onTouchCancelCapture'], - 'capture-bindtouchstart': ['onTouchStartCapture'], - 'capture-bindtouchmove': ['onTouchMoveCapture'], - 'capture-bindtouchend': ['onTouchEndCapture'], - 'capture-bindtouchcancel': ['onTouchCancelCapture'], - 'capture-catchtap': ['onTouchStartCapture', 'onTouchMoveCapture', 'onTouchEndCapture'], - 'capture-catchlongpress': ['onTouchStartCapture', 'onTouchMoveCapture', 'onTouchEndCapture', 'onTouchCancelCapture'], - 'capture-catchtouchstart': ['onTouchStartCapture'], - 'capture-catchtouchmove': ['onTouchMoveCapture'], - 'capture-catchtouchend': ['onTouchEndCapture'], - 'capture-catchtouchcancel': ['onTouchCancelCapture'] +const eventConfigMap: { [key: string]: { bitFlag: string; events: string[] } } = { + bindtap: { bitFlag: '0', events: ['onTouchStart', 'onTouchMove', 'onTouchEnd'] }, + bindlongpress: { bitFlag: '1', events: ['onTouchStart', 'onTouchMove', 'onTouchEnd', 'onTouchCancel'] }, + bindtouchstart: { bitFlag: '2', events: ['onTouchStart'] }, + bindtouchmove: { bitFlag: '3', events: ['onTouchMove'] }, + bindtouchend: { bitFlag: '4', events: ['onTouchEnd'] }, + bindtouchcancel: { bitFlag: '5', events: ['onTouchCancel'] }, + catchtap: { bitFlag: '6', events: ['onTouchStart', 'onTouchMove', 'onTouchEnd'] }, + catchlongpress: { bitFlag: '7', events: ['onTouchStart', 'onTouchMove', 'onTouchEnd', 'onTouchCancel'] }, + catchtouchstart: { bitFlag: '8', events: ['onTouchStart'] }, + catchtouchmove: { bitFlag: '9', events: ['onTouchMove'] }, + catchtouchend: { bitFlag: 'a', events: ['onTouchEnd'] }, + catchtouchcancel: { bitFlag: 'b', events: ['onTouchCancel'] }, + 'capture-bindtap': { bitFlag: 'c', events: ['onTouchStartCapture', 'onTouchMoveCapture', 'onTouchEndCapture'] }, + 'capture-bindlongpress': { bitFlag: 'd', events: ['onTouchStartCapture', 'onTouchMoveCapture', 'onTouchEndCapture', 'onTouchCancelCapture'] }, + 'capture-bindtouchstart': { bitFlag: 'e', events: ['onTouchStartCapture'] }, + 'capture-bindtouchmove': { bitFlag: 'f', events: ['onTouchMoveCapture'] }, + 'capture-bindtouchend': { bitFlag: 'g', events: ['onTouchEndCapture'] }, + 'capture-bindtouchcancel': { bitFlag: 'h', events: ['onTouchCancelCapture'] }, + 'capture-catchtap': { bitFlag: 'i', events: ['onTouchStartCapture', 'onTouchMoveCapture', 'onTouchEndCapture'] }, + 'capture-catchlongpress': { bitFlag: 'j', events: ['onTouchStartCapture', 'onTouchMoveCapture', 'onTouchEndCapture', 'onTouchCancelCapture'] }, + 'capture-catchtouchstart': { bitFlag: 'k', events: ['onTouchStartCapture'] }, + 'capture-catchtouchmove': { bitFlag: 'l', events: ['onTouchMoveCapture'] }, + 'capture-catchtouchend': { bitFlag: 'm', events: ['onTouchEndCapture'] }, + 'capture-catchtouchcancel': { bitFlag: 'n', events: ['onTouchCancelCapture'] } } - export default eventConfigMap diff --git a/packages/webpack-plugin/lib/runtime/components/react/getInnerListeners.ts b/packages/webpack-plugin/lib/runtime/components/react/getInnerListeners.ts index 31085eacdc..10f0ae1301 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/getInnerListeners.ts +++ b/packages/webpack-plugin/lib/runtime/components/react/getInnerListeners.ts @@ -1,4 +1,4 @@ -import { useRef } from 'react' +import { useRef, useMemo, RefObject } from 'react' import { hasOwn, collectDataset } from '@mpxjs/utils' import { omit, extendObject } from './utils' import eventConfigMap from './event.config' @@ -20,26 +20,16 @@ const getTouchEvent = ( config: UseInnerPropsConfig ) => { const nativeEvent = event.nativeEvent - const { - timestamp, - pageX, - pageY, - touches, - changedTouches - } = nativeEvent + const { timestamp, pageX, pageY, touches, changedTouches } = nativeEvent const { id } = props const { layoutRef } = config - const currentTarget = extendObject( - {}, - event.currentTarget, - { - id: id || '', - dataset: collectDataset(props), - offsetLeft: layoutRef?.current?.offsetLeft || 0, - offsetTop: layoutRef?.current?.offsetTop || 0 - } - ) + const currentTarget = extendObject({}, event.currentTarget, { + id: id || '', + dataset: collectDataset(props), + offsetLeft: layoutRef?.current?.offsetLeft || 0, + offsetTop: layoutRef?.current?.offsetTop || 0 + }) const pendingProps = (event as any)._targetInst?.pendingProps || {} @@ -61,7 +51,7 @@ const getTouchEvent = ( x: pageX, y: pageY }, - touches: touches.map(item => { + touches: touches.map((item) => { return { identifier: item.identifier, pageX: item.pageX, @@ -70,7 +60,7 @@ const getTouchEvent = ( clientY: item.locationY } }), - changedTouches: changedTouches.map(item => { + changedTouches: changedTouches.map((item) => { return { identifier: item.identifier, pageX: item.pageX, @@ -88,7 +78,10 @@ const getTouchEvent = ( export const getCustomEvent = ( type = '', oe: any = {}, - { detail = {}, layoutRef }: { detail?: Record; layoutRef: LayoutRef }, + { + detail = {}, + layoutRef + }: { detail?: Record; layoutRef?: LayoutRef }, props: Props = {} ) => { const targetInfo = extendObject({}, oe.target, { @@ -107,6 +100,168 @@ export const getCustomEvent = ( }) } +function handleEmitEvent ( + events: string[], + type: string, + oe: NativeTouchEvent, + propsRef: Record, + config: UseInnerPropsConfig +) { + events.forEach((event) => { + if (propsRef.current[event]) { + const match = /^(catch|capture-catch):?(.*?)(?:\.(.*))?$/.exec(event) + if (match) { + oe.stopPropagation() + } + propsRef.current[event]( + getTouchEvent(type, oe, propsRef.current, config) + ) + } + }) +} + +function checkIsNeedPress (e: NativeTouchEvent, type: 'bubble' | 'capture', ref: RefObject) { + const tapDetailInfo = ref.current!.mpxPressInfo.detail || { x: 0, y: 0 } + const nativeEvent = e.nativeEvent + const currentPageX = nativeEvent.changedTouches[0].pageX + const currentPageY = nativeEvent.changedTouches[0].pageY + if ( + Math.abs(currentPageX - tapDetailInfo.x) > 1 || + Math.abs(currentPageY - tapDetailInfo.y) > 1 + ) { + ref.current!.needPress[type] = false + ref.current!.startTimer[type] && + clearTimeout(ref.current!.startTimer[type] as SetTimeoutReturnType) + ref.current!.startTimer[type] = null + } +} + +function handleTouchstart (e: NativeTouchEvent, type: 'bubble' | 'capture', ref: RefObject, propsRef: Record, config: UseInnerPropsConfig) { + e.persist() + const bubbleTouchEvent = ['catchtouchstart', 'bindtouchstart'] + const bubblePressEvent = ['catchlongpress', 'bindlongpress'] + const captureTouchEvent = [ + 'capture-catchtouchstart', + 'capture-bindtouchstart' + ] + const capturePressEvent = [ + 'capture-catchlongpress', + 'capture-bindlongpress' + ] + ref.current!.startTimer[type] = null + ref.current!.needPress[type] = true + const nativeEvent = e.nativeEvent + ref.current!.mpxPressInfo.detail = { + x: nativeEvent.changedTouches[0].pageX, + y: nativeEvent.changedTouches[0].pageY + } + const currentTouchEvent = + type === 'bubble' ? bubbleTouchEvent : captureTouchEvent + const currentPressEvent = + type === 'bubble' ? bubblePressEvent : capturePressEvent + handleEmitEvent(currentTouchEvent, 'touchstart', e, propsRef, config) + const { + catchlongpress, + bindlongpress, + 'capture-catchlongpress': captureCatchlongpress, + 'capture-bindlongpress': captureBindlongpress + } = propsRef.current + if ( + catchlongpress || + bindlongpress || + captureCatchlongpress || + captureBindlongpress + ) { + ref.current!.startTimer[type] = setTimeout(() => { + ref.current!.needPress[type] = false + handleEmitEvent(currentPressEvent, 'longpress', e, propsRef, config) + }, 350) + } +} + +function handleTouchmove (e: NativeTouchEvent, type: 'bubble' | 'capture', ref: RefObject, propsRef: Record, config: UseInnerPropsConfig) { + const bubbleTouchEvent = ['catchtouchmove', 'bindtouchmove'] + const captureTouchEvent = [ + 'capture-catchtouchmove', + 'capture-bindtouchmove' + ] + const currentTouchEvent = + type === 'bubble' ? bubbleTouchEvent : captureTouchEvent + handleEmitEvent(currentTouchEvent, 'touchmove', e, propsRef, config) + checkIsNeedPress(e, type, ref) +} + +function handleTouchend (e: NativeTouchEvent, type: 'bubble' | 'capture', ref: RefObject, propsRef: Record, config: UseInnerPropsConfig) { + // move event may not be triggered + checkIsNeedPress(e, type, ref) + const bubbleTouchEvent = ['catchtouchend', 'bindtouchend'] + const bubbleTapEvent = ['catchtap', 'bindtap'] + const captureTouchEvent = [ + 'capture-catchtouchend', + 'capture-bindtouchend' + ] + const captureTapEvent = ['capture-catchtap', 'capture-bindtap'] + const currentTouchEvent = + type === 'bubble' ? bubbleTouchEvent : captureTouchEvent + const currentTapEvent = + type === 'bubble' ? bubbleTapEvent : captureTapEvent + ref.current!.startTimer[type] && + clearTimeout(ref.current!.startTimer[type] as SetTimeoutReturnType) + ref.current!.startTimer[type] = null + handleEmitEvent(currentTouchEvent, 'touchend', e, propsRef, config) + if (ref.current!.needPress[type]) { + if (type === 'bubble' && config.disableTap) { + return + } + handleEmitEvent(currentTapEvent, 'tap', e, propsRef, config) + } +} + +function handleTouchcancel ( + e: NativeTouchEvent, + type: 'bubble' | 'capture', + ref: RefObject, propsRef: Record, config: UseInnerPropsConfig +) { + const bubbleTouchEvent = ['catchtouchcancel', 'bindtouchcancel'] + const captureTouchEvent = [ + 'capture-catchtouchcancel', + 'capture-bindtouchcancel' + ] + const currentTouchEvent = + type === 'bubble' ? bubbleTouchEvent : captureTouchEvent + ref.current!.startTimer[type] && + clearTimeout(ref.current!.startTimer[type] as SetTimeoutReturnType) + ref.current!.startTimer[type] = null + handleEmitEvent(currentTouchEvent, 'touchcancel', e, propsRef, config) +} + +function createTouchEventHandler (eventName: 'onTouchStart'|'onTouchMove'|'onTouchEnd'|'onTouchCancel', type: 'bubble' | 'capture') { + return (e: NativeTouchEvent, ref: RefObject, propsRef: Record, config: UseInnerPropsConfig) => { + const handlerMap = { + onTouchStart: handleTouchstart, + onTouchMove: handleTouchmove, + onTouchEnd: handleTouchend, + onTouchCancel: handleTouchcancel + } + + const handler = handlerMap[eventName] + if (handler) { + handler(e, type, ref, propsRef, config) + } + } +} + +const touchEventList = [ + { eventName: 'onTouchStart', handler: createTouchEventHandler('onTouchStart', 'bubble') }, + { eventName: 'onTouchMove', handler: createTouchEventHandler('onTouchMove', 'bubble') }, + { eventName: 'onTouchEnd', handler: createTouchEventHandler('onTouchEnd', 'bubble') }, + { eventName: 'onTouchCancel', handler: createTouchEventHandler('onTouchCancel', 'bubble') }, + { eventName: 'onTouchStartCapture', handler: createTouchEventHandler('onTouchStart', 'capture') }, + { eventName: 'onTouchMoveCapture', handler: createTouchEventHandler('onTouchMove', 'capture') }, + { eventName: 'onTouchEndCapture', handler: createTouchEventHandler('onTouchEnd', 'capture') }, + { eventName: 'onTouchCancelCapture', handler: createTouchEventHandler('onTouchCancel', 'capture') } +] + const useInnerProps = ( props: Props = {}, additionalProps: AdditionalProps = {}, @@ -132,7 +287,10 @@ const useInnerProps = ( const propsRef = useRef>({}) const eventConfig: { [key: string]: string[] } = {} - const config = rawConfig || { layoutRef: { current: {} }, disableTouch: false, disableTap: false } + const config = rawConfig || { + layoutRef: { current: {} }, + disableTap: false + } const removeProps = [ 'children', 'enable-background', @@ -147,166 +305,39 @@ const useInnerProps = ( propsRef.current = extendObject({}, props, additionalProps) + let hashEventKey = '' + const rawEventKeys: Array = [] + for (const key in eventConfigMap) { if (hasOwn(propsRef.current, key)) { - eventConfig[key] = eventConfigMap[key] + eventConfig[key] = eventConfigMap[key].events + hashEventKey = hashEventKey + eventConfigMap[key].bitFlag + rawEventKeys.push(key) } } - if (!(Object.keys(eventConfig).length) || config.disableTouch) { - return omit(propsRef.current, removeProps) - } - - function handleEmitEvent ( - events: string[], - type: string, - oe: NativeTouchEvent - ) { - events.forEach(event => { - if (propsRef.current[event]) { - const match = /^(catch|capture-catch):?(.*?)(?:\.(.*))?$/.exec(event) - if (match) { - oe.stopPropagation() - } - propsRef.current[event](getTouchEvent(type, oe, propsRef.current, config)) - } - }) - } - - function checkIsNeedPress (e: NativeTouchEvent, type: 'bubble' | 'capture') { - const tapDetailInfo = ref.current.mpxPressInfo.detail || { x: 0, y: 0 } - const nativeEvent = e.nativeEvent - const currentPageX = nativeEvent.changedTouches[0].pageX - const currentPageY = nativeEvent.changedTouches[0].pageY - if (Math.abs(currentPageX - tapDetailInfo.x) > 1 || Math.abs(currentPageY - tapDetailInfo.y) > 1) { - ref.current.needPress[type] = false - ref.current.startTimer[type] && clearTimeout(ref.current.startTimer[type] as SetTimeoutReturnType) - ref.current.startTimer[type] = null - } - } - - function handleTouchstart (e: NativeTouchEvent, type: 'bubble' | 'capture') { - e.persist() - const bubbleTouchEvent = ['catchtouchstart', 'bindtouchstart'] - const bubblePressEvent = ['catchlongpress', 'bindlongpress'] - const captureTouchEvent = ['capture-catchtouchstart', 'capture-bindtouchstart'] - const capturePressEvent = ['capture-catchlongpress', 'capture-bindlongpress'] - ref.current.startTimer[type] = null - ref.current.needPress[type] = true - const nativeEvent = e.nativeEvent - ref.current.mpxPressInfo.detail = { - x: nativeEvent.changedTouches[0].pageX, - y: nativeEvent.changedTouches[0].pageY - } - const currentTouchEvent = type === 'bubble' ? bubbleTouchEvent : captureTouchEvent - const currentPressEvent = type === 'bubble' ? bubblePressEvent : capturePressEvent - handleEmitEvent(currentTouchEvent, 'touchstart', e) - const { catchlongpress, bindlongpress, 'capture-catchlongpress': captureCatchlongpress, 'capture-bindlongpress': captureBindlongpress } = propsRef.current - if (catchlongpress || bindlongpress || captureCatchlongpress || captureBindlongpress) { - ref.current.startTimer[type] = setTimeout(() => { - ref.current.needPress[type] = false - handleEmitEvent(currentPressEvent, 'longpress', e) - }, 350) + const events = useMemo(() => { + if (!rawEventKeys.length) { + return {} } - } - - function handleTouchmove (e: NativeTouchEvent, type: 'bubble' | 'capture') { - const bubbleTouchEvent = ['catchtouchmove', 'bindtouchmove'] - const captureTouchEvent = ['capture-catchtouchmove', 'capture-bindtouchmove'] - const currentTouchEvent = type === 'bubble' ? bubbleTouchEvent : captureTouchEvent - handleEmitEvent(currentTouchEvent, 'touchmove', e) - checkIsNeedPress(e, type) - } - - function handleTouchend (e: NativeTouchEvent, type: 'bubble' | 'capture') { - // move event may not be triggered - checkIsNeedPress(e, type) - const bubbleTouchEvent = ['catchtouchend', 'bindtouchend'] - const bubbleTapEvent = ['catchtap', 'bindtap'] - const captureTouchEvent = ['capture-catchtouchend', 'capture-bindtouchend'] - const captureTapEvent = ['capture-catchtap', 'capture-bindtap'] - const currentTouchEvent = type === 'bubble' ? bubbleTouchEvent : captureTouchEvent - const currentTapEvent = type === 'bubble' ? bubbleTapEvent : captureTapEvent - ref.current.startTimer[type] && clearTimeout(ref.current.startTimer[type] as SetTimeoutReturnType) - ref.current.startTimer[type] = null - handleEmitEvent(currentTouchEvent, 'touchend', e) - if (ref.current.needPress[type]) { - if (type === 'bubble' && config.disableTap) { - return + const transformedEventKeys = rawEventKeys.reduce((acc: string[], key) => { + if (propsRef.current[key]) { + return acc.concat(eventConfig[key]) } - handleEmitEvent(currentTapEvent, 'tap', e) - } - } - - function handleTouchcancel (e: NativeTouchEvent, type: 'bubble' | 'capture') { - const bubbleTouchEvent = ['catchtouchcancel', 'bindtouchcancel'] - const captureTouchEvent = ['capture-catchtouchcancel', 'capture-bindtouchcancel'] - const currentTouchEvent = type === 'bubble' ? bubbleTouchEvent : captureTouchEvent - ref.current.startTimer[type] && clearTimeout(ref.current.startTimer[type] as SetTimeoutReturnType) - ref.current.startTimer[type] = null - handleEmitEvent(currentTouchEvent, 'touchcancel', e) - } + return acc + }, []) + const finalEventKeys = [...new Set(transformedEventKeys)] + const events: Record void> = {} - const touchEventList = [{ - eventName: 'onTouchStart', - handler: (e: NativeTouchEvent) => { - handleTouchstart(e, 'bubble') - } - }, { - eventName: 'onTouchMove', - handler: (e: NativeTouchEvent) => { - handleTouchmove(e, 'bubble') - } - }, { - eventName: 'onTouchEnd', - handler: (e: NativeTouchEvent) => { - handleTouchend(e, 'bubble') - } - }, { - eventName: 'onTouchCancel', - handler: (e: NativeTouchEvent) => { - handleTouchcancel(e, 'bubble') - } - }, { - eventName: 'onTouchStartCapture', - handler: (e: NativeTouchEvent) => { - handleTouchstart(e, 'capture') - } - }, { - eventName: 'onTouchMoveCapture', - handler: (e: NativeTouchEvent) => { - handleTouchmove(e, 'capture') - } - }, { - eventName: 'onTouchEndCapture', - handler: (e: NativeTouchEvent) => { - handleTouchend(e, 'capture') - } - }, { - eventName: 'onTouchCancelCapture', - handler: (e: NativeTouchEvent) => { - handleTouchcancel(e, 'capture') - } - }] - - const events: Record void> = {} - - let transformedEventKeys: string[] = [] - for (const key in eventConfig) { - if (propsRef.current[key]) { - transformedEventKeys = transformedEventKeys.concat(eventConfig[key]) - } - } - - const finalEventKeys = [...new Set(transformedEventKeys)] - - touchEventList.forEach(item => { - if (finalEventKeys.includes(item.eventName)) { - events[item.eventName] = item.handler - } - }) + touchEventList.forEach((item) => { + if (finalEventKeys.includes(item.eventName)) { + events[item.eventName] = (e: NativeTouchEvent) => + item.handler(e, ref, propsRef, config) + } + }) - const rawEventKeys = Object.keys(eventConfig) + return events + }, [hashEventKey]) return extendObject( {}, diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-button.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-button.tsx index 74d95812ff..004afef42c 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-button.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-button.tsx @@ -34,7 +34,7 @@ * ✘ bindagreeprivacyauthorization * ✔ bindtap */ -import { useEffect, useRef, useState, ReactNode, forwardRef, useContext, JSX } from 'react' +import { createElement, useEffect, useRef, useState, ReactNode, forwardRef, useContext, JSX } from 'react' import { View, StyleSheet, @@ -442,21 +442,16 @@ const Button = forwardRef, ButtonProps>((buttonPro } ) - return ( - - {loading && } + return createElement(View, innerProps, loading && createElement(Loading, { alone: !children }), + wrapChildren( + props, { - wrapChildren( - props, - { - hasVarDec, - varContext: varContextRef.current, - textStyle, - textProps - } - ) + hasVarDec, + varContext: varContextRef.current, + textStyle, + textProps } - + ) ) }) diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-canvas/index.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-canvas/index.tsx index 69075d99c5..b5b31cc3d1 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-canvas/index.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-canvas/index.tsx @@ -9,7 +9,7 @@ * ✔ bindlongtap * ✔ binderror */ -import React, { useRef, useState, useCallback, useEffect, forwardRef, JSX, TouchEvent, MutableRefObject } from 'react' +import React, { createElement, useRef, useState, useCallback, useEffect, forwardRef, JSX, TouchEvent, MutableRefObject } from 'react' import { View, Platform, StyleSheet, NativeSyntheticEvent } from 'react-native' import { WebView } from 'react-native-webview' import useNodesRef, { HandlerRef } from '../useNodesRef' @@ -249,54 +249,48 @@ const _Canvas = forwardRef, CanvasPr if (Platform.OS === 'android') { const isAndroid9 = Platform.Version >= 28 - return ( - - { - if (canvasRef.current) { - canvasRef.current.webview = element - } - }} - style={[ - isAndroid9 ? stylesheet.webviewAndroid9 : stylesheet.webview, - { height, width } - ]} - source={{ html }} - originWhitelist={originWhitelist} - onMessage={onMessage} - onLoad={onLoad} - overScrollMode="never" - mixedContentMode="always" - scalesPageToFit={false} - javaScriptEnabled - domStorageEnabled - thirdPartyCookiesEnabled - allowUniversalAccessFromFileURLs - /> - - ) - } - - return ( - - { + return createElement(View, innerProps, createElement( + WebView, + { + ref: (element) => { if (canvasRef.current) { canvasRef.current.webview = element } - }} - style={[stylesheet.webview, { height, width }]} - source={{ html }} - originWhitelist={originWhitelist} - onMessage={onMessage} - onLoad={onLoad} - scrollEnabled={false} - /> - - ) + }, + style: [ + isAndroid9 ? stylesheet.webviewAndroid9 : stylesheet.webview, + { height, width } + ], + source: { html }, + originWhitelist: originWhitelist, + onMessage: onMessage, + onLoad: onLoad, + overScrollMode: 'never', + mixedContentMode: 'always', + scalesPageToFit: false, + javaScriptEnabled: true, + domStorageEnabled: true, + thirdPartyCookiesEnabled: true, + allowUniversalAccessFromFileURLs: true + }) + ) + } + + return createElement(View, innerProps, createElement(WebView, { + ref: (element) => { + if (canvasRef.current) { + canvasRef.current.webview = element + } + }, + style: [stylesheet.webview, { height, width }], + source: { html }, + originWhitelist: originWhitelist, + onMessage: onMessage, + onLoad: onLoad, + scrollEnabled: false + })) }) + _Canvas.displayName = 'mpxCanvas' export default _Canvas diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-checkbox-group.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-checkbox-group.tsx index fcdffee350..5c59d52f50 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-checkbox-group.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-checkbox-group.tsx @@ -8,7 +8,8 @@ import { ReactNode, useContext, useMemo, - useEffect + useEffect, + createElement } from 'react' import { View, @@ -156,20 +157,21 @@ const CheckboxGroup = forwardRef< notifyChange } }, []) - return ( - - + + return createElement( + View, + innerProps, + createElement( + CheckboxGroupContext.Provider, + { value: contextValue }, + wrapChildren( + props, { - wrapChildren( - props, - { - hasVarDec, - varContext: varContextRef.current - } - ) + hasVarDec, + varContext: varContextRef.current } - - + ) + ) ) }) diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-checkbox.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-checkbox.tsx index 48aa98d273..3da976c34f 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-checkbox.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-checkbox.tsx @@ -13,7 +13,8 @@ import { ReactNode, useContext, Dispatch, - SetStateAction + SetStateAction, + createElement } from 'react' import { View, @@ -206,28 +207,26 @@ const Checkbox = forwardRef, CheckboxProps>( } }, [checked]) - return ( - - - - + return createElement(View, innerProps, + createElement( + View, + { style: defaultStyle }, + createElement(Icon, { + type: 'success_no_circle', + size: 18, + color: disabled ? '#ADADAD' : color, + style: isChecked ? styles.iconChecked : styles.icon + }) + ), + wrapChildren( + props, { - wrapChildren( - props, - { - hasVarDec, - varContext: varContextRef.current, - textStyle, - textProps - } - ) + hasVarDec, + varContext: varContextRef.current, + textStyle, + textProps } - + ) ) } ) diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-form.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-form.tsx index defda9083f..100a321eae 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-form.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-form.tsx @@ -5,7 +5,7 @@ * ✔ bindreset */ import { View } from 'react-native' -import { JSX, useRef, forwardRef, ReactNode, useMemo, useCallback } from 'react' +import { JSX, useRef, forwardRef, ReactNode, useMemo, createElement } from 'react' import useNodesRef, { HandlerRef } from './useNodesRef' import useInnerProps, { getCustomEvent } from './getInnerListeners' import { FormContext } from './context' @@ -102,25 +102,20 @@ const _Form = forwardRef, FormProps>((fromProps: For reset } }, []) - return ( - - - { - wrapChildren( - props, - { - hasVarDec, - varContext: varContextRef.current, - textStyle, - textProps - } - ) - } - - - ) + + return createElement(View, innerProps, createElement( + FormContext.Provider, + { value: contextValue }, + wrapChildren( + props, + { + hasVarDec, + varContext: varContextRef.current, + textStyle, + textProps + } + ) + )) }) _Form.displayName = 'MpxForm' diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-icon.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-icon.tsx index e26d89558b..d1a4edeceb 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-icon.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-icon.tsx @@ -3,7 +3,7 @@ * ✔ size * ✔ color */ -import { JSX, forwardRef, useRef } from 'react' +import { JSX, forwardRef, useRef, createElement } from 'react' import { Text, TextStyle, Image } from 'react-native' import useInnerProps from './getInnerListeners' import useNodesRef, { HandlerRef } from './useNodesRef' @@ -93,7 +93,7 @@ const Icon = forwardRef, IconProps>( } ) - return + return createElement(Image, innerProps) } ) diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-image.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-image.tsx index 998dddd179..96bd53f19d 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-image.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-image.tsx @@ -10,7 +10,7 @@ * ✔ bindtap * ✔ DEFAULT_SIZE */ -import { useEffect, useMemo, useState, useRef, forwardRef } from 'react' +import { useEffect, useMemo, useState, useRef, forwardRef, createElement } from 'react' import { Image as RNImage, View, @@ -403,35 +403,31 @@ const Image = forwardRef, ImageProps>((props, re } ) - return ( - - { - isSvg - ? - : loaded && renderImage({ - source: { uri: src }, - resizeMode: resizeMode, - onLoad: bindload && onImageLoad, - onError: binderror && onImageError, - style: extendObject( - { - transformOrigin: 'top left', - width: isCropMode ? imageWidth : '100%', - height: isCropMode ? imageHeight : '100%' - }, - isCropMode ? modeStyle : {} - ) - }, enableFastImage) - } - + return createElement(View, innerProps, + isSvg + ? createElement(SvgCssUri, { + uri: src, + onLayout: onSvgLoad, + onError: binderror && onSvgError, + style: extendObject( + { transformOrigin: 'top left' }, + modeStyle + ) + }) + : loaded && renderImage({ + source: { uri: src }, + resizeMode: resizeMode, + onLoad: bindload && onImageLoad, + onError: binderror && onImageError, + style: extendObject( + { + transformOrigin: 'top left', + width: isCropMode ? imageWidth : '100%', + height: isCropMode ? imageHeight : '100%' + }, + isCropMode ? modeStyle : {} + ) + }, enableFastImage) ) }) diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-input.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-input.tsx index e8a466affd..be27905bbb 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-input.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-input.tsx @@ -37,7 +37,7 @@ * ✘ bind:keyboardcompositionend * ✘ bind:onkeyboardheightchange */ -import { JSX, forwardRef, useMemo, useRef, useState, useContext, useEffect } from 'react' +import { JSX, forwardRef, useMemo, useRef, useState, useContext, useEffect, createElement } from 'react' import { KeyboardTypeOptions, Platform, @@ -187,7 +187,7 @@ const Input = forwardRef, FinalInputProps { padding: 0, backgroundColor: '#fff' }, style, multiline && autoHeight - ? { height: Math.max((style as any)?.minHeight || 35, contentHeight) } + ? { minHeight: Math.max((style as any)?.minHeight || 35, contentHeight) } : {} ) @@ -405,7 +405,22 @@ const Input = forwardRef, FinalInputProps { ref: nodeRef, style: extendObject({}, normalStyle, layoutStyle), - allowFontScaling + allowFontScaling, + keyboardType: keyboardType, + secureTextEntry: !!password, + defaultValue: defaultValue, + value: inputValue, + maxLength: maxlength === -1 ? undefined : maxlength, + editable: !disabled, + autoFocus: !!autoFocus || !!focus, + returnKeyType: confirmType, + selection: selection, + selectionColor: cursorColor, + blurOnSubmit: !multiline && !confirmHold, + underlineColorAndroid: 'rgba(0,0,0,0)', + textAlignVertical: textAlignVertical, + placeholderTextColor: placeholderTextColor, + multiline: !!multiline }, layoutProps, { @@ -413,16 +428,17 @@ const Input = forwardRef, FinalInputProps onBlur: bindblur && onInputBlur, onKeyPress: bindconfirm && onKeyPress, onSubmitEditing: bindconfirm && multiline && onSubmitEditing, - onSelectionChange: bindselectionchange && onSelectionChange + onSelectionChange: bindselectionchange && onSelectionChange, + onTextInput: onTextInput, + onChange: onChange, + onContentSizeChange: onContentSizeChange } ), [ 'type', - 'keyboardType', 'password', 'placeholder-style', 'disabled', - 'maxlength', 'auto-focus', 'focus', 'confirm-type', @@ -430,37 +446,13 @@ const Input = forwardRef, FinalInputProps 'cursor', 'cursor-color', 'selection-start', - 'selection-end', - 'multiline' + 'selection-end' ], { layoutRef } ) - - return ( - - ) + return createElement(TextInput, innerProps) }) Input.displayName = 'MpxInput' diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-label.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-label.tsx index 4c4eb5879c..4ef3fe16ae 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-label.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-label.tsx @@ -1,7 +1,7 @@ /** * ✘ for */ -import { JSX, useRef, forwardRef, ReactNode, useCallback } from 'react' +import { JSX, useRef, forwardRef, ReactNode, useCallback, createElement } from 'react' import { View, ViewStyle, NativeSyntheticEvent } from 'react-native' import { noop, warn } from '@mpxjs/utils' import useInnerProps, { getCustomEvent } from './getInnerListeners' @@ -92,21 +92,19 @@ const Label = forwardRef, LabelProps>( } ) - return - + return createElement(View, innerProps, createElement( + LabelContext.Provider, + { value: contextRef }, + wrapChildren( + props, { - wrapChildren( - props, - { - hasVarDec, - varContext: varContextRef.current, - textStyle, - textProps - } - ) + hasVarDec, + varContext: varContextRef.current, + textStyle, + textProps } - - + ) + )) } ) diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-movable-area.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-movable-area.tsx index b354fc3591..3e937be817 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-movable-area.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-movable-area.tsx @@ -3,7 +3,7 @@ */ import { View } from 'react-native' -import { JSX, forwardRef, ReactNode, useRef, useMemo } from 'react' +import { JSX, forwardRef, ReactNode, useRef, useMemo, createElement } from 'react' import useNodesRef, { HandlerRef } from './useNodesRef' import useInnerProps from './getInnerListeners' import { MovableAreaContext } from './context' @@ -51,23 +51,17 @@ const _MovableArea = forwardRef, MovableAreaP ref: movableViewRef }, layoutProps), [], { layoutRef }) - return ( - - + return createElement(MovableAreaContext.Provider, { value: contextValue }, createElement( + View, + innerProps, + wrapChildren( + props, { - wrapChildren( - props, - { - hasVarDec, - varContext: varContextRef.current - } - ) + hasVarDec, + varContext: varContextRef.current } - - - ) + ) + )) }) _MovableArea.displayName = 'MpxMovableArea' diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-movable-view.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-movable-view.tsx index a5b29a9a37..3a0a330251 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-movable-view.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-movable-view.tsx @@ -17,12 +17,12 @@ * ✔ htouchmove * ✔ vtouchmove */ -import { useEffect, forwardRef, ReactNode, useContext, useCallback, useRef, useMemo } from 'react' +import { useEffect, forwardRef, ReactNode, useContext, useCallback, useRef, useMemo, createElement } from 'react' import { StyleSheet, NativeSyntheticEvent, View, LayoutChangeEvent } from 'react-native' import { getCustomEvent } from './getInnerListeners' import useNodesRef, { HandlerRef } from './useNodesRef' import { MovableAreaContext } from './context' -import { useTransformStyle, splitProps, splitStyle, HIDDEN_STYLE, wrapChildren, GestureHandler, flatGesture } from './utils' +import { useTransformStyle, splitProps, splitStyle, HIDDEN_STYLE, wrapChildren, GestureHandler, flatGesture, extendObject } from './utils' import { GestureDetector, Gesture, GestureTouchEvent, GestureStateChangeEvent, PanGestureHandlerEventPayload, PanGesture } from 'react-native-gesture-handler' import Animated, { useSharedValue, @@ -518,28 +518,24 @@ const _MovableView = forwardRef, MovableViewP const catchEventHandlers = injectCatchEvent(props) const layoutStyle = !hasLayoutRef.current && hasSelfPercent ? HIDDEN_STYLE : {} - return ( - - - { - wrapChildren( - props, - { - hasVarDec, - varContext: varContextRef.current, - textStyle, - textProps - } - ) - } - - - ) + + return createElement(GestureDetector, { gesture: gesture }, createElement( + Animated.View, + extendObject({ + ref: nodeRef, + onLayout: onLayout, + style: [innerStyle, animatedStyles, layoutStyle] + }, catchEventHandlers), + wrapChildren( + props, + { + hasVarDec, + varContext: varContextRef.current, + textStyle, + textProps + } + ) + )) }) _MovableView.displayName = 'MpxMovableView' diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-navigator.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-navigator.tsx index 90d3465f44..3de5cfd553 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-navigator.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-navigator.tsx @@ -8,7 +8,7 @@ * ✔ delta */ import { View } from 'react-native' -import { useCallback, forwardRef, JSX } from 'react' +import { useCallback, forwardRef, JSX, createElement } from 'react' import useInnerProps from './getInnerListeners' import { redirectTo, navigateTo, navigateBack, reLaunch, switchTab } from '@mpxjs/api-proxy' @@ -53,13 +53,7 @@ const _Navigator = forwardRef((props, ref): JSX.Element = bindtap: handleClick }) - return ( - - {children} - - ) + return createElement(MpxView, innerProps, children) }) _Navigator.displayName = 'MpxNavigator' diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-radio-group.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-radio-group.tsx index 5ded44937e..2c1c35db03 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-radio-group.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-radio-group.tsx @@ -8,7 +8,8 @@ import { ReactNode, useContext, useMemo, - useEffect + useEffect, + createElement } from 'react' import { View, @@ -154,20 +155,17 @@ const radioGroup = forwardRef< } ) - return ( - - - { - wrapChildren( - props, - { - hasVarDec, - varContext: varContextRef.current - } - ) - } - - + return createElement(View, innerProps, createElement( + RadioGroupContext.Provider, + { value: contextValue }, + wrapChildren( + props, + { + hasVarDec, + varContext: varContextRef.current + } + ) + ) ) }) diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-radio.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-radio.tsx index c1662f5cc8..dd760fcec0 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-radio.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-radio.tsx @@ -4,7 +4,7 @@ * ✔ checked * ✔ color */ -import { JSX, useRef, useState, forwardRef, useEffect, ReactNode, useContext, Dispatch, SetStateAction } from 'react' +import { JSX, useRef, useState, forwardRef, useEffect, ReactNode, useContext, Dispatch, SetStateAction, createElement } from 'react' import { View, StyleSheet, ViewStyle, NativeSyntheticEvent } from 'react-native' import { warn } from '@mpxjs/utils' import { LabelContext, RadioGroupContext } from './context' @@ -193,32 +193,26 @@ const Radio = forwardRef, RadioProps>( } }, [checked]) - return ( - - - - + return createElement(View, innerProps, + createElement( + View, + { style: defaultStyle }, + createElement(Icon, { + type: 'success', + size: 24, + color: disabled ? '#E1E1E1' : color, + style: extendObject({}, styles.icon, isChecked && styles.iconChecked, disabled && styles.iconDisabled) + }) + ), + wrapChildren( + props, { - wrapChildren( - props, - { - hasVarDec, - varContext: varContextRef.current, - textStyle, - textProps - } - ) + hasVarDec, + varContext: varContextRef.current, + textStyle, + textProps } - + ) ) } ) diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-rich-text/html.ts b/packages/webpack-plugin/lib/runtime/components/react/mpx-rich-text/html.ts new file mode 100644 index 0000000000..222797da33 --- /dev/null +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-rich-text/html.ts @@ -0,0 +1,40 @@ + +export const generateHTML = (html: string) => { + return ` + + + +
${html}
+ +` +} diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-rich-text/index.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-rich-text/index.tsx new file mode 100644 index 0000000000..db6097df5d --- /dev/null +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-rich-text/index.tsx @@ -0,0 +1,121 @@ + +/** + * ✔ nodes + */ +import { View, ViewProps, ViewStyle } from 'react-native' +import { useRef, forwardRef, JSX, useState } from 'react' +import useInnerProps from '../getInnerListeners' +import useNodesRef, { HandlerRef } from '../useNodesRef' // 引入辅助函数 +import { useTransformStyle, useLayout } from '../utils' +import { WebView, WebViewMessageEvent } from 'react-native-webview' +import { generateHTML } from './html' + +type Node = { + type: 'node' | 'text' + name?: string + attrs?: any + children?: Array + text: string +} + +interface _RichTextProps extends ViewProps { + style?: ViewStyle + nodes: string | Array + 'enable-var'?: boolean + 'external-var-context'?: Record + 'parent-font-size'?: number + 'parent-width'?: number + 'parent-height'?: number +} + +function jsonToHtmlStr (elements: Array) { + let htmlStr = '' + + for (const element of elements) { + if (element.type === 'text') { + htmlStr += element.text + return htmlStr + } + + const { name, attrs = {}, children = [] } = element + + let attrStr = '' + for (const [key, value] of Object.entries(attrs)) attrStr += ` ${key}="${value}"` + + let childrenStr = '' + for (const child of children) childrenStr += jsonToHtmlStr([child]) + + htmlStr += `<${name}${attrStr}>${childrenStr}` + } + + return htmlStr +} + +const _RichText = forwardRef, _RichTextProps>((props, ref): JSX.Element => { + const { + style = {}, + nodes, + 'enable-var': enableVar, + 'external-var-context': externalVarContext, + 'parent-font-size': parentFontSize, + 'parent-width': parentWidth, + 'parent-height': parentHeight + } = props + + const nodeRef = useRef(null) + const [webViewHeight, setWebViewHeight] = useState(0) + + const { + normalStyle, + hasSelfPercent, + setWidth, + setHeight + } = useTransformStyle(Object.assign({ + width: '100%', + height: webViewHeight + }, style), { + enableVar, + externalVarContext, + parentFontSize, + parentWidth, + parentHeight + }) + + const { + layoutRef, + layoutStyle, + layoutProps + } = useLayout({ props, hasSelfPercent, setWidth, setHeight, nodeRef }) + + useNodesRef(props, ref, nodeRef, { + layoutRef + }) + + const innerProps = useInnerProps(props, { + ref: nodeRef, + style: { ...normalStyle, ...layoutStyle }, + ...layoutProps + }, [], { + layoutRef + }) + + const html: string = typeof nodes === 'string' ? nodes : jsonToHtmlStr(nodes) + + return ( + + { + setWebViewHeight(+event.nativeEvent.data) + }} + > + + + ) +}) + +_RichText.displayName = 'mpx-rich-text' + +export default _RichText diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-root-portal.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-root-portal.tsx index 46827be068..7c5d53769d 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-root-portal.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-root-portal.tsx @@ -1,7 +1,7 @@ /** * ✔ enable */ -import { ReactNode } from 'react' +import { ReactNode, createElement, Fragment } from 'react' import { Portal } from '@ant-design/react-native' import { warn } from '@mpxjs/utils' interface RootPortalProps { @@ -16,10 +16,8 @@ const _RootPortal = (props: RootPortalProps) => { warn('The root-portal component does not support the style prop.') } return enable - ? - {children} - - : <>{children} + ? createElement(Portal, null, children) + : createElement(Fragment, null, children) } _RootPortal.displayName = 'MpxRootPortal' diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-scroll-view.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-scroll-view.tsx index 17f45a6e41..b058e622dc 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-scroll-view.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-scroll-view.tsx @@ -33,7 +33,7 @@ */ import { ScrollView } from 'react-native-gesture-handler' import { View, RefreshControl, NativeSyntheticEvent, NativeScrollEvent, LayoutChangeEvent, ViewStyle } from 'react-native' -import { JSX, ReactNode, RefObject, useRef, useState, useEffect, forwardRef, useContext } from 'react' +import { JSX, ReactNode, RefObject, useRef, useState, useEffect, forwardRef, useContext, createElement } from 'react' import { useAnimatedRef } from 'react-native-reanimated' import { warn } from '@mpxjs/utils' import useInnerProps, { getCustomEvent } from './getInnerListeners' @@ -165,7 +165,7 @@ const _ScrollView = forwardRef, S const initialTimeout = useRef | null>(null) const intersectionObservers = useContext(IntersectionObserverContext) - const snapScrollIntoView = useRef('') + const firstScrollIntoViewChange = useRef(false) const { normalStyle, @@ -220,23 +220,28 @@ const _ScrollView = forwardRef, S }, [refresherTriggered]) useEffect(() => { - if (scrollIntoView && __selectRef && snapScrollIntoView.current !== scrollIntoView) { - snapScrollIntoView.current = scrollIntoView || '' - setTimeout(() => { - const refs = __selectRef(`#${scrollIntoView}`, 'node') - if (refs) { - const { nodeRef } = refs.getNodeInstance() - nodeRef.current?.measureLayout( - scrollViewRef.current, - (left: number, top:number) => { - scrollToOffset(left, top) - } - ) - } - }) + if (scrollIntoView && __selectRef) { + if (!firstScrollIntoViewChange.current) { + setTimeout(handleScrollIntoView) + } else { + handleScrollIntoView() + } } + firstScrollIntoViewChange.current = true }, [scrollIntoView]) + function handleScrollIntoView () { + const refs = __selectRef!(`#${scrollIntoView}`, 'node') + if (!refs) return + const { nodeRef } = refs.getNodeInstance() + nodeRef.current?.measureLayout( + scrollViewRef.current, + (left: number, top:number) => { + scrollToOffset(left, top) + } + ) + } + function selectLength (size: { height: number; width: number }) { return !scrollX ? size.height : size.width } @@ -491,32 +496,26 @@ const _ScrollView = forwardRef, S white: ['#fff'] } - return ( - - ) - : undefined} - > - { - wrapChildren( - props, - { - hasVarDec, - varContext: varContextRef.current, - textStyle, - textProps - } - ) + return createElement( + ScrollView, + extendObject({}, innerProps, { + refreshControl: refresherEnabled + ? createElement(RefreshControl, extendObject({ + progressBackgroundColor: refresherBackground, + refreshing: refreshing, + onRefresh: onRefresh + }, (refresherDefaultStyle && refresherDefaultStyle !== 'none' ? { colors: refreshColor[refresherDefaultStyle] } : null))) + : undefined + }), + wrapChildren( + props, + { + hasVarDec, + varContext: varContextRef.current, + textStyle, + textProps } - + ) ) }) diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-switch.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-switch.tsx index 026424c30c..7a1c008071 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-switch.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-switch.tsx @@ -5,7 +5,7 @@ * ✔ color */ import { Switch, SwitchProps, ViewStyle, NativeSyntheticEvent } from 'react-native' -import { useRef, useEffect, forwardRef, JSX, useState, useContext } from 'react' +import { useRef, useEffect, forwardRef, JSX, useState, useContext, createElement } from 'react' import { warn } from '@mpxjs/utils' import useNodesRef, { HandlerRef } from './useNodesRef' // 引入辅助函数 import useInnerProps, { getCustomEvent } from './getInnerListeners' @@ -136,22 +136,26 @@ const _Switch = forwardRef, _SwitchProps>((prop }) if (type === 'checkbox') { - return + return createElement( + CheckBox, + extendObject({}, innerProps, { + color: color, + style: normalStyle, + checked: isChecked + }) + ) } - return + return createElement( + Switch, + extendObject({}, innerProps, { + style: normalStyle, + value: isChecked, + trackColor: { false: '#FFF', true: color }, + thumbColor: isChecked ? '#FFF' : '#f4f3f4', + ios_backgroundColor: '#FFF' + }) + ) }) _Switch.displayName = 'MpxSwitch' diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-text.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-text.tsx index 6962dc9657..e3364de844 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-text.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-text.tsx @@ -5,7 +5,7 @@ * ✘ decode */ import { Text, TextStyle, TextProps } from 'react-native' -import { useRef, forwardRef, ReactNode, JSX } from 'react' +import { useRef, forwardRef, ReactNode, JSX, createElement } from 'react' import useInnerProps from './getInnerListeners' import useNodesRef, { HandlerRef } from './useNodesRef' // 引入辅助函数 import { useTransformStyle, wrapChildren } from './utils' @@ -65,21 +65,13 @@ const _Text = forwardRef, _TextProps>((props, ref): layoutRef }) - return ( - - { - wrapChildren( - props, - { - hasVarDec, - varContext: varContextRef.current - } - ) - } - - ) + return createElement(Text, innerProps, wrapChildren( + props, + { + hasVarDec, + varContext: varContextRef.current + } + )) }) _Text.displayName = 'MpxText' diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-textarea.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-textarea.tsx index 35bb8403e5..24e5907be5 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-textarea.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-textarea.tsx @@ -9,10 +9,10 @@ * ✘ show-confirm-bar * ✔ bindlinechange: No `heightRpx` info. */ -import { JSX, forwardRef } from 'react' +import { JSX, forwardRef, createElement } from 'react' import { Keyboard, TextInput } from 'react-native' import Input, { InputProps, PrivateInputProps } from './mpx-input' -import { omit } from './utils' +import { omit, extendObject } from './utils' import { HandlerRef } from './useNodesRef' export type TextareProps = Omit< @@ -29,14 +29,15 @@ const Textarea = forwardRef, TextareProps>( 'multiline', 'confirm-hold' ]) - return ( - Keyboard.dismiss()} - {...restProps} - /> + + return createElement( + Input, + extendObject({ + ref: ref, + multiline: true, + confirmType: 'next', + bindblur: () => Keyboard.dismiss() + }, restProps) ) } ) diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-view.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-view.tsx index 42b9d0a077..cdc8c02aef 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-view.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-view.tsx @@ -5,7 +5,7 @@ * ✔ hover-stay-time */ import { View, TextStyle, NativeSyntheticEvent, ViewProps, ImageStyle, StyleSheet, Image, LayoutChangeEvent } from 'react-native' -import { useRef, useState, useEffect, forwardRef, ReactNode, JSX } from 'react' +import { useRef, useState, useEffect, forwardRef, ReactNode, JSX, createElement } from 'react' import useInnerProps from './getInnerListeners' import Animated from 'react-native-reanimated' import useAnimationHooks from './useAnimationHooks' @@ -13,6 +13,7 @@ import type { AnimationProp } from './useAnimationHooks' import { ExtendedViewStyle } from './types/common' import useNodesRef, { HandlerRef } from './useNodesRef' import { parseUrl, PERCENT_REGEX, splitStyle, splitProps, useTransformStyle, wrapChildren, useLayout, renderImage, pickStyle, extendObject } from './utils' +import { error } from '@mpxjs/utils' import LinearGradient from 'react-native-linear-gradient' export interface _ViewProps extends ViewProps { @@ -716,7 +717,7 @@ const _View = forwardRef, _ViewProps>((viewProps, r enableBackground = enableBackground || !!backgroundStyle const enableBackgroundRef = useRef(enableBackground) if (enableBackgroundRef.current !== enableBackground) { - throw new Error('[Mpx runtime error]: background use should be stable in the component lifecycle, or you can set [enable-background] with true.') + error('[Mpx runtime error]: background use should be stable in the component lifecycle, or you can set [enable-background] with true.') } const nodeRef = useRef(null) @@ -774,13 +775,13 @@ const _View = forwardRef, _ViewProps>((viewProps, r enableAnimation = enableAnimation || !!animation const enableAnimationRef = useRef(enableAnimation) if (enableAnimationRef.current !== enableAnimation) { - throw new Error('[Mpx runtime error]: animation use should be stable in the component lifecycle, or you can set [enable-animation] with true.') + error('[Mpx runtime error]: animation use should be stable in the component lifecycle, or you can set [enable-animation] with true.') } - const finalStyle = enableAnimation - ? useAnimationHooks({ - animation, - style: viewStyle - }) + const finalStyle = enableAnimationRef.current + ? [viewStyle, useAnimationHooks({ + animation, + style: viewStyle + })] : viewStyle const innerProps = useInnerProps( props, @@ -814,17 +815,10 @@ const _View = forwardRef, _ViewProps>((viewProps, r innerStyle, enableFastImage }) + return enableAnimation - ? ( - {childNode} - ) - : ( - {childNode} - ) + ? createElement(Animated.View, innerProps, childNode) + : createElement(View, innerProps, childNode) }) _View.displayName = 'MpxView' diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-web-view.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-web-view.tsx index 79038d756a..b645d05094 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-web-view.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-web-view.tsx @@ -1,6 +1,5 @@ -import { forwardRef, JSX, useEffect, useRef, useContext, useMemo } from 'react' -import { noop, warn } from '@mpxjs/utils' -import { View } from 'react-native' +import { forwardRef, JSX, useRef, useContext, useMemo, createElement } from 'react' +import { warn, getFocusedNavigation, isFunction } from '@mpxjs/utils' import { Portal } from '@ant-design/react-native' import { getCustomEvent } from './getInnerListeners' import { promisify, redirectTo, navigateTo, navigateBack, reLaunch, switchTab } from '@mpxjs/api-proxy' @@ -31,26 +30,25 @@ interface WebViewProps { } interface PayloadData { - data?: Record + [x: string]: any } type MessageData = { payload?: PayloadData, + args?: Array, type?: string, callbackId?: number } -const _WebView = forwardRef, WebViewProps>((props, ref): JSX.Element => { - const { src, bindmessage = noop, bindload = noop, binderror = noop } = props - if (!src) { - return () - } +const _WebView = forwardRef, WebViewProps>((props, ref): JSX.Element | null => { + const { src, bindmessage, bindload, binderror } = props + const mpx = global.__mpx if (props.style) { warn('The web-view component does not support the style prop.') } const pageId = useContext(RouteContext) const currentPage = useMemo(() => getCurrentPage(pageId), [pageId]) - + const webViewRef = useRef(null) const defaultWebViewStyle = { position: 'absolute' as 'absolute' | 'relative' | 'static', left: 0 as number, @@ -59,34 +57,14 @@ const _WebView = forwardRef, WebViewProps>((pr bottom: 0 as number } - const webViewRef = useRef(null) useNodesRef(props, ref, webViewRef, { style: defaultWebViewStyle }) - const _messageList = useRef([]) - const handleUnload = () => { - // 这里是 WebView 销毁前执行的逻辑 - bindmessage(getCustomEvent('messsage', {}, { - detail: { - data: _messageList.current - }, - layoutRef: webViewRef - })) + if (!src) { + return null } - useEffect(() => { - if (currentPage) { - currentPage.__webViewUrl = src - } - }, [src, currentPage]) - - useEffect(() => { - // 组件卸载时执行 - return () => { - handleUnload() - } - }, []) const _load = function (res: WebViewNavigationEvent) { const result = { type: 'load', @@ -107,8 +85,33 @@ const _WebView = forwardRef, WebViewProps>((pr } binderror(result) } + const injectedJavaScript = ` + if (window.ReactNativeWebView && window.ReactNativeWebView.postMessage) { + var _documentTitle = document.title; + window.ReactNativeWebView.postMessage(JSON.stringify({ + type: 'setTitle', + payload: { + _documentTitle: _documentTitle + } + })) + Object.defineProperty(document, 'title', { + set (val) { + _documentTitle = val + window.ReactNativeWebView.postMessage(JSON.stringify({ + type: 'setTitle', + payload: { + _documentTitle: _documentTitle + } + })) + }, + get () { + return _documentTitle + } + }); + } + ` const _changeUrl = function (navState: WebViewNavigation) { - if (currentPage) { + if (navState.navigationType) { // navigationType这个事件在页面开始加载时和页面加载完成时都会被触发所以判断这个避免其他无效触发执行该逻辑 currentPage.__webViewUrl = navState.url } } @@ -121,43 +124,79 @@ const _WebView = forwardRef, WebViewProps>((pr if (typeof nativeEventData === 'string') { data = JSON.parse(nativeEventData) } - } catch (e) { - data = {} - } + } catch (e) {} + const args = data.args const postData: PayloadData = data.payload || {} - switch (data.type) { + const params = Array.isArray(args) ? args : [postData] + const type = data.type + switch (type) { + case 'setTitle': + { // case下不允许直接声明,包个块解决该问题 + const title = postData._documentTitle + if (title) { + const navigation = getFocusedNavigation() + navigation && navigation.setOptions({ title }) + } + } + break case 'postMessage': - _messageList.current.push(postData.data) + bindmessage && bindmessage(getCustomEvent('messsage', {}, { // RN组件销毁顺序与小程序不一致,所以改成和支付宝消息一致 + detail: { + data: params[0]?.data + } + })) asyncCallback = Promise.resolve({ errMsg: 'invokeWebappApi:ok' }) break case 'navigateTo': - asyncCallback = navObj.navigateTo(postData) + asyncCallback = navObj.navigateTo(...params) break case 'navigateBack': - asyncCallback = navObj.navigateBack(postData) + asyncCallback = navObj.navigateBack(...params) break case 'redirectTo': - asyncCallback = navObj.redirectTo(postData) + asyncCallback = navObj.redirectTo(...params) break case 'switchTab': - asyncCallback = navObj.switchTab(postData) + asyncCallback = navObj.switchTab(...params) break case 'reLaunch': - asyncCallback = navObj.reLaunch(postData) + asyncCallback = navObj.reLaunch(...params) + break + default: + if (type) { + const implement = mpx.config.webviewConfig.apiImplementations && mpx.config.webviewConfig.apiImplementations[type] + if (isFunction(implement)) { + asyncCallback = Promise.resolve(implement(...params)) + } else { + /* eslint-disable prefer-promise-reject-errors */ + asyncCallback = Promise.reject({ + errMsg: `未在apiImplementations中配置${type}方法` + }) + } + } break } asyncCallback && asyncCallback.then((res: any) => { if (webViewRef.current?.postMessage) { const test = JSON.stringify({ - type: data.type, + type, callbackId: data.callbackId, result: res }) webViewRef.current.postMessage(test) } + }).catch((error: any) => { + if (webViewRef.current?.postMessage) { + const test = JSON.stringify({ + type, + callbackId: data.callbackId, + error + }) + webViewRef.current.postMessage(test) + } }) } const events = {} @@ -172,21 +211,17 @@ const _WebView = forwardRef, WebViewProps>((pr onError: _error }) } - if (bindmessage) { - extendObject(events, { - onMessage: _message - }) - } - return ( - - ) + extendObject(events, { + onMessage: _message + }) + + return createElement(Portal, null, createElement(WebView, extendObject({ + style: defaultWebViewStyle, + source: { uri: src }, + ref: webViewRef, + javaScriptEnabled: true, + onNavigationStateChange: _changeUrl + }, events))) }) _WebView.displayName = 'MpxWebview' diff --git a/packages/webpack-plugin/lib/runtime/components/react/types/global.d.ts b/packages/webpack-plugin/lib/runtime/components/react/types/global.d.ts index ed58b3fb4c..052fc1ac56 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/types/global.d.ts +++ b/packages/webpack-plugin/lib/runtime/components/react/types/global.d.ts @@ -19,7 +19,8 @@ declare module '@mpxjs/utils' { bottom: number left: number right: number - } + }, + setOptions: (params: Record) => void } | undefined } diff --git a/packages/webpack-plugin/lib/runtime/components/react/useAnimationHooks.ts b/packages/webpack-plugin/lib/runtime/components/react/useAnimationHooks.ts index 9eb2d97cc5..77d1ac0503 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/useAnimationHooks.ts +++ b/packages/webpack-plugin/lib/runtime/components/react/useAnimationHooks.ts @@ -83,9 +83,31 @@ const InitialValue: ExtendedViewStyle = Object.assign({ const TransformOrigin = 'transformOrigin' // transform const isTransform = (key: string) => Object.keys(TransformInitial).includes(key) +// 多value解析 +const parseValues = (str: string, char = ' ') => { + let stack = 0 + let temp = '' + const result = [] + for (let i = 0; i < str.length; i++) { + if (str[i] === '(') { + stack++ + } else if (str[i] === ')') { + stack-- + } + // 非括号内 或者 非分隔字符且非空 + if (stack !== 0 || (str[i] !== char && str[i] !== ' ')) { + temp += str[i] + } + if ((stack === 0 && str[i] === char) || i === str.length - 1) { + result.push(temp) + temp = '' + } + } + return result +} // parse string transform, eg: transform: 'rotateX(45deg) rotateZ(0.785398rad)' const parseTransform = (transformStr: string) => { - const values = transformStr.trim().split(/\s+/) + const values = parseValues(transformStr) const transform: {[propName: string]: string|number|number[]}[] = [] values.forEach(item => { const match = item.match(/([/\w]+)\(([^)]+)\)/) @@ -109,7 +131,7 @@ const parseTransform = (transformStr: string) => { break case 'matrix': case 'matrix3d': - transform.push({ [key]: val.split(',').map(val => +val) }) + transform.push({ [key]: parseValues(val, ',').map(val => +val) }) break case 'translate': case 'scale': @@ -120,8 +142,8 @@ const parseTransform = (transformStr: string) => { { // 2 个以上的值处理 key = key.replace('3d', '') - const vals = val.split(',', key === 'rotate' ? 4 : 3) - // scale(.5) === scaleX(.5) scaleY(.5) 这里处理一下 + const vals = parseValues(val, ',').splice(0, key === 'rotate' ? 4 : 3) + // scale(.5) === scaleX(.5) scaleY(.5) if (vals.length === 1 && key === 'scale') { vals.push(vals[0]) } @@ -218,12 +240,15 @@ export default function useAnimationHooks (props: _ViewProps) { } // 添加每个key的多次step动画 animatedKeys.forEach(key => { - let toVal = (rules.get(key) || transform.get(key)) // key不存在,第一轮取shareValMap[key]value,非第一轮取上一轮的 - if (toVal === undefined) { - toVal = index > 0 ? lastValueMap[key] : shareValMap[key].value - } - const animation = getAnimation({ key, value: toVal }, { delay, duration, easing }, needSetCallback ? setTransformOrigin : undefined) + const toVal = rules.get(key) !== undefined + ? rules.get(key) + : transform.get(key) !== undefined + ? transform.get(key) + : index > 0 + ? lastValueMap[key] + : shareValMap[key].value + const animation = getAnimation({ key, value: toVal! }, { delay, duration, easing }, needSetCallback ? setTransformOrigin : undefined) needSetCallback = false if (!sequence[key]) { sequence[key] = [animation] @@ -231,7 +256,7 @@ export default function useAnimationHooks (props: _ViewProps) { sequence[key].push(animation) } // 更新一下 lastValueMap - lastValueMap[key] = toVal + lastValueMap[key] = toVal! }) // 赋值驱动动画 animatedKeys.forEach((key) => { @@ -327,6 +352,6 @@ export default function useAnimationHooks (props: _ViewProps) { styles[key] = shareValMap[key].value } return styles - }, Object.assign({}, originalStyle) as ExtendedViewStyle) + }, {} as ExtendedViewStyle) }) } diff --git a/packages/webpack-plugin/lib/runtime/components/react/utils.tsx b/packages/webpack-plugin/lib/runtime/components/react/utils.tsx index a00bb771ea..99a171d7ec 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/utils.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/utils.tsx @@ -18,8 +18,10 @@ export const HIDDEN_STYLE = { opacity: 0 } -const varDecRegExp = /^--.*/ +const varDecRegExp = /^--/ const varUseRegExp = /var\(/ +const unoVarDecRegExp = /^--un-/ +const unoVarUseRegExp = /var\(--un-/ const calcUseRegExp = /calc\(/ const envUseRegExp = /env\(/ @@ -37,7 +39,7 @@ function getSafeAreaInset (name: string) { } export function omit (obj: T, fields: K[]): Omit { - const shallowCopy: any = Object.assign({}, obj) + const shallowCopy: any = extendObject({}, obj) for (let i = 0; i < fields.length; i += 1) { const key = fields[i] delete shallowCopy[key] @@ -77,7 +79,7 @@ export const parseInlineStyle = (inlineStyle = ''): Record => { const [k, v, ...rest] = style.split(':') if (rest.length || !v || !k) return styleObj const key = k.trim().replace(/-./g, c => c.substring(1).toUpperCase()) - return Object.assign(styleObj, { [key]: global.__formatValue(v.trim()) }) + return extendObject(styleObj, { [key]: global.__formatValue(v.trim()) }) }, {}) } @@ -286,11 +288,15 @@ interface TransformStyleConfig { export function useTransformStyle (styleObj: Record = {}, { enableVar, externalVarContext, parentFontSize, parentWidth, parentHeight }: TransformStyleConfig) { const varStyle: Record = {} + const unoVarStyle: Record = {} const normalStyle: Record = {} + const normalStyleRef = useRef>({}) + const normalStyleChangedRef = useRef(false) let hasVarDec = false let hasVarUse = false let hasSelfPercent = false const varKeyPaths: Array> = [] + const unoVarKeyPaths: Array> = [] const percentKeyPaths: Array> = [] const calcKeyPaths: Array> = [] const envKeyPaths: Array> = [] @@ -299,7 +305,9 @@ export function useTransformStyle (styleObj: Record = {}, { enableV function varVisitor ({ key, value, keyPath }: VisitorArg) { if (keyPath.length === 1) { - if (varDecRegExp.test(key)) { + if (unoVarDecRegExp.test(key)) { + unoVarStyle[key] = value + } else if (varDecRegExp.test(key)) { hasVarDec = true varStyle[key] = value } else { @@ -308,25 +316,31 @@ export function useTransformStyle (styleObj: Record = {}, { enableV } } // 对于var定义中使用的var无需替换值,可以通过resolveVar递归解析出值 - if (!varDecRegExp.test(key) && varUseRegExp.test(value)) { - hasVarUse = true - varKeyPaths.push(keyPath.slice()) + if (!varDecRegExp.test(key)) { + // 一般情况下一个样式属性中不会混用unocss var和普通css var,可分开进行互斥处理 + if (unoVarUseRegExp.test(value)) { + unoVarKeyPaths.push(keyPath.slice()) + } else if (varUseRegExp.test(value)) { + hasVarUse = true + varKeyPaths.push(keyPath.slice()) + } } } - // traverse var + // traverse var & generate normalStyle traverseStyle(styleObj, [varVisitor]) + hasVarDec = hasVarDec || !!externalVarContext enableVar = enableVar || hasVarDec || hasVarUse const enableVarRef = useRef(enableVar) if (enableVarRef.current !== enableVar) { error('css variable use/declare should be stable in the component lifecycle, or you can set [enable-var] with true.') } - // apply var + // apply css var const varContextRef = useRef({}) if (enableVarRef.current) { const varContext = useContext(VarContext) - const newVarContext = Object.assign({}, varContext, externalVarContext, varStyle) + const newVarContext = extendObject({}, varContext, externalVarContext, varStyle) // 缓存比较newVarContext是否发生变化 if (diffAndCloneA(varContextRef.current, newVarContext).diff) { varContextRef.current = newVarContext @@ -334,70 +348,86 @@ export function useTransformStyle (styleObj: Record = {}, { enableV transformVar(normalStyle, varKeyPaths, varContextRef.current) } - function envVisitor ({ value, keyPath }: VisitorArg) { - if (envUseRegExp.test(value)) { - envKeyPaths.push(keyPath.slice()) - } + // apply unocss var + if (unoVarKeyPaths.length) { + transformVar(normalStyle, unoVarKeyPaths, unoVarStyle) } - function calcVisitor ({ value, keyPath }: VisitorArg) { - if (calcUseRegExp.test(value)) { - calcKeyPaths.push(keyPath.slice()) - } + const { clone, diff } = diffAndCloneA(normalStyle, normalStyleRef.current) + if (diff) { + normalStyleRef.current = clone + normalStyleChangedRef.current = !normalStyleChangedRef.current } - function percentVisitor ({ key, value, keyPath }: VisitorArg) { - if (hasOwn(selfPercentRule, key) && PERCENT_REGEX.test(value)) { - hasSelfPercent = true - percentKeyPaths.push(keyPath.slice()) - } else if ((key === 'fontSize' || key === 'lineHeight') && PERCENT_REGEX.test(value)) { - percentKeyPaths.push(keyPath.slice()) + const memoResult = useMemo(() => { + // transform can be memoized + function envVisitor ({ value, keyPath }: VisitorArg) { + if (envUseRegExp.test(value)) { + envKeyPaths.push(keyPath.slice()) + } } - } - // traverse env & calc & percent - traverseStyle(normalStyle, [envVisitor, percentVisitor, calcVisitor]) + function calcVisitor ({ value, keyPath }: VisitorArg) { + if (calcUseRegExp.test(value)) { + calcKeyPaths.push(keyPath.slice()) + } + } - const percentConfig = { - width, - height, - fontSize: normalStyle.fontSize, - parentWidth, - parentHeight, - parentFontSize - } + function percentVisitor ({ key, value, keyPath }: VisitorArg) { + if (hasOwn(selfPercentRule, key) && PERCENT_REGEX.test(value)) { + hasSelfPercent = true + percentKeyPaths.push(keyPath.slice()) + } else if ((key === 'fontSize' || key === 'lineHeight') && PERCENT_REGEX.test(value)) { + percentKeyPaths.push(keyPath.slice()) + } + } - // apply env - transformEnv(normalStyle, envKeyPaths) - // apply percent - transformPercent(normalStyle, percentKeyPaths, percentConfig) - // apply calc - transformCalc(normalStyle, calcKeyPaths, (value: string, key: string) => { - if (PERCENT_REGEX.test(value)) { - const resolved = resolvePercent(value, key, percentConfig) - return typeof resolved === 'number' ? resolved : 0 - } else { - const formatted = global.__formatValue(value) - if (typeof formatted === 'number') { - return formatted + // traverse env & calc & percent + traverseStyle(normalStyle, [envVisitor, percentVisitor, calcVisitor]) + + const percentConfig = { + width, + height, + fontSize: normalStyle.fontSize, + parentWidth, + parentHeight, + parentFontSize + } + + // apply env + transformEnv(normalStyle, envKeyPaths) + // apply percent + transformPercent(normalStyle, percentKeyPaths, percentConfig) + // apply calc + transformCalc(normalStyle, calcKeyPaths, (value: string, key: string) => { + if (PERCENT_REGEX.test(value)) { + const resolved = resolvePercent(value, key, percentConfig) + return typeof resolved === 'number' ? resolved : 0 } else { - warn('calc() only support number, px, rpx, % temporarily.') - return 0 + const formatted = global.__formatValue(value) + if (typeof formatted === 'number') { + return formatted + } else { + warn('calc() only support number, px, rpx, % temporarily.') + return 0 + } } + }) + // transform number enum stringify + transformStringify(normalStyle) + + return { + normalStyle, + hasSelfPercent } - }) - // transform number enum stringify - transformStringify(normalStyle) + }, [normalStyleChangedRef.current, width, height, parentWidth, parentHeight, parentFontSize]) - return { - normalStyle, - hasSelfPercent, + return extendObject({ hasVarDec, - enableVarRef, varContextRef, setWidth, setHeight - } + }, memoResult) } export interface VisitorArg { @@ -565,7 +595,7 @@ export const useStableCallback = ( } export const usePrevious = (value: T): T | undefined => { - const ref = useRef(undefined) + const ref = useRef() const prev = ref.current ref.current = value return prev diff --git a/packages/webpack-plugin/lib/runtime/components/web/event.js b/packages/webpack-plugin/lib/runtime/components/web/event.js deleted file mode 100644 index 41b6d9d5aa..0000000000 --- a/packages/webpack-plugin/lib/runtime/components/web/event.js +++ /dev/null @@ -1,105 +0,0 @@ -import { extendEvent } from './getInnerListeners' -import { isBrowser } from '../../env' - -function MpxEvent (layer) { - this.targetElement = null - - this.touches = [] - - this.touchStartX = 0 - - this.touchStartY = 0 - - this.startTimer = null - - this.needTap = true - - this.isTouchDevice = document && ('ontouchstart' in document.documentElement) - - this.onTouchStart = (event) => { - if (event.targetTouches?.length > 1) { - return true - } - - this.touches = event.targetTouches - this.targetElement = event.target - this.needTap = true - this.startTimer = null - this.touchStartX = this.touches[0].pageX - this.touchStartY = this.touches[0].pageY - this.startTimer = setTimeout(() => { - this.needTap = false - this.sendEvent(this.targetElement, 'longpress', event) - this.sendEvent(this.targetElement, 'longtap', event) - }, 350) - } - - this.onTouchMove = (event) => { - const touch = event.changedTouches[0] - if (Math.abs(touch.pageX - this.touchStartX) > 1 || Math.abs(touch.pageY - this.touchStartY) > 1) { - this.needTap = false - this.startTimer && clearTimeout(this.startTimer) - this.startTimer = null - } - } - - this.onTouchEnd = (event) => { - if (event.targetTouches?.length > 1) { - return true - } - this.startTimer && clearTimeout(this.startTimer) - this.startTimer = null - if (this.needTap) { - this.sendEvent(this.targetElement, 'tap', event) - } - } - - this.onClick = (event) => { - this.targetElement = event.target - this.sendEvent(this.targetElement, 'tap', event) - } - this.sendEvent = (targetElement, type, event) => { - const touchEvent = new CustomEvent(type, { - bubbles: true, - cancelable: true - }) - const changedTouches = event.changedTouches || [] - extendEvent(touchEvent, { - timeStamp: event.timeStamp, - changedTouches, - touches: changedTouches, - detail: { - // pc端点击事件可能没有changedTouches,所以直接从 event中取 - x: changedTouches[0]?.pageX || event.pageX || 0, - y: changedTouches[0]?.pageY || event.pageY || 0 - } - }) - targetElement && targetElement.dispatchEvent(touchEvent) - } - - this.addListener = () => { - if (this.isTouchDevice) { - layer.addEventListener('touchstart', this.onTouchStart, true) - layer.addEventListener('touchmove', this.onTouchMove, true) - layer.addEventListener('touchend', this.onTouchEnd, true) - } else { - layer.addEventListener('click', this.onClick, true) - } - } - this.addListener() -} - -export function createEvent () { - if (isBrowser && !global.__mpxCreatedEvent) { - global.__mpxCreatedEvent = true - if (document.readyState === 'complete' || document.readyState === 'interactive') { - // eslint-disable-next-line no-new - new MpxEvent(document.body) - } else { - document.addEventListener('DOMContentLoaded', function () { - // eslint-disable-next-line no-new - new MpxEvent(document.body) - }, false) - } - } -} diff --git a/packages/webpack-plugin/lib/runtime/components/web/mpx-web-view.vue b/packages/webpack-plugin/lib/runtime/components/web/mpx-web-view.vue index b3fecfa9b6..0e1ff52a63 100644 --- a/packages/webpack-plugin/lib/runtime/components/web/mpx-web-view.vue +++ b/packages/webpack-plugin/lib/runtime/components/web/mpx-web-view.vue @@ -5,7 +5,8 @@