@mpxjs/webpack-plugin 2.10.15-5 → 2.10.15-prelease.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/lib/dependencies/DynamicEntryDependency.js +1 -1
  2. package/lib/dependencies/ImportDependency.js +102 -0
  3. package/lib/{retry-runtime-module.js → dependencies/RetryRuntimeModule.js} +1 -1
  4. package/lib/index.js +13 -15
  5. package/lib/platform/template/wx/component-config/progress.js +12 -0
  6. package/lib/platform/template/wx/component-config/slider.js +12 -0
  7. package/lib/platform/template/wx/component-config/unsupported.js +1 -1
  8. package/lib/platform/template/wx/index.js +3 -1
  9. package/lib/resolver/AddEnvPlugin.js +13 -0
  10. package/lib/resolver/AddModePlugin.js +18 -0
  11. package/lib/runtime/components/react/dist/getInnerListeners.js +35 -21
  12. package/lib/runtime/components/react/dist/mpx-movable-view.jsx +102 -34
  13. package/lib/runtime/components/react/dist/mpx-portal/portal-manager.jsx +3 -5
  14. package/lib/runtime/components/react/dist/mpx-progress.jsx +163 -0
  15. package/lib/runtime/components/react/dist/mpx-scroll-view.jsx +38 -7
  16. package/lib/runtime/components/react/dist/mpx-slider.jsx +321 -0
  17. package/lib/runtime/components/react/dist/mpx-swiper.jsx +9 -16
  18. package/lib/runtime/components/react/dist/mpx-view.jsx +7 -10
  19. package/lib/runtime/components/react/dist/mpx-web-view.jsx +20 -1
  20. package/lib/runtime/components/react/getInnerListeners.ts +41 -22
  21. package/lib/runtime/components/react/mpx-movable-view.tsx +156 -48
  22. package/lib/runtime/components/react/mpx-portal/portal-manager.tsx +4 -8
  23. package/lib/runtime/components/react/mpx-progress.tsx +259 -0
  24. package/lib/runtime/components/react/mpx-scroll-view.tsx +39 -7
  25. package/lib/runtime/components/react/mpx-slider.tsx +444 -0
  26. package/lib/runtime/components/react/mpx-swiper.tsx +9 -16
  27. package/lib/runtime/components/react/mpx-view.tsx +7 -10
  28. package/lib/runtime/components/react/mpx-web-view.tsx +22 -1
  29. package/lib/runtime/components/react/types/getInnerListeners.d.ts +7 -2
  30. package/lib/runtime/components/web/mpx-input.vue +14 -0
  31. package/lib/runtime/components/web/mpx-movable-area.vue +43 -19
  32. package/lib/runtime/components/web/mpx-movable-view.vue +93 -3
  33. package/lib/runtime/components/web/mpx-scroll-view.vue +7 -1
  34. package/lib/runtime/components/web/mpx-swiper.vue +1 -2
  35. package/lib/runtime/components/web/mpx-video.vue +12 -1
  36. package/lib/runtime/components/web/mpx-web-view.vue +3 -3
  37. package/lib/runtime/optionProcessor.js +3 -1
  38. package/lib/runtime/optionProcessorReact.js +4 -2
  39. package/lib/template-compiler/compiler.js +69 -35
  40. package/lib/utils/chain-assign.js +47 -0
  41. package/lib/utils/check-core-version-match.js +75 -15
  42. package/lib/wxs/pre-loader.js +5 -5
  43. package/lib/wxss/utils.js +1 -1
  44. package/package.json +3 -2
  45. package/lib/dependencies/ImportDependencyTemplate.js +0 -50
@@ -6,7 +6,7 @@ const toPosix = require('../utils/to-posix')
6
6
  const async = require('async')
7
7
  const parseRequest = require('../utils/parse-request')
8
8
  const hasOwn = require('../utils/has-own')
9
- const { RetryRuntimeGlobal } = require('../retry-runtime-module')
9
+ const { RetryRuntimeGlobal } = require('./RetryRuntimeModule')
10
10
 
11
11
  class DynamicEntryDependency extends NullDependency {
12
12
  constructor (range, request, entryType, outputPath = '', packageRoot = '', relativePath = '', context = '', extraOptions = {}) {
@@ -0,0 +1,102 @@
1
+ const Dependency = require('webpack/lib/Dependency')
2
+ const makeSerializable = require('webpack/lib/util/makeSerializable')
3
+ const ModuleDependency = require('webpack/lib/dependencies/ModuleDependency')
4
+ const { RetryRuntimeGlobal } = require('./RetryRuntimeModule')
5
+
6
+ class ImportDependency extends ModuleDependency {
7
+ /**
8
+ * @param {string} request the request
9
+ * @param {[number, number]} range expression range
10
+ * @param {string[][]=} referencedExports list of referenced exports
11
+ */
12
+ constructor (request, range, referencedExports, extraOptions) {
13
+ super(request)
14
+ this.range = range
15
+ this.referencedExports = referencedExports
16
+ this.extraOptions = extraOptions
17
+ }
18
+
19
+ get type () {
20
+ return 'import()'
21
+ }
22
+
23
+ get category () {
24
+ return 'esm'
25
+ }
26
+
27
+ /**
28
+ * Returns list of exports referenced by this dependency
29
+ * @param {ModuleGraph} moduleGraph module graph
30
+ * @param {RuntimeSpec} runtime the runtime for which the module is analysed
31
+ * @returns {(string[] | ReferencedExport)[]} referenced exports
32
+ */
33
+ getReferencedExports (moduleGraph, runtime) {
34
+ return this.referencedExports
35
+ ? this.referencedExports.map((e) => ({
36
+ name: e,
37
+ canMangle: false
38
+ }))
39
+ : Dependency.EXPORTS_OBJECT_REFERENCED
40
+ }
41
+
42
+ serialize (context) {
43
+ context.write(this.range)
44
+ context.write(this.referencedExports)
45
+ context.write(this.extraOptions)
46
+ super.serialize(context)
47
+ }
48
+
49
+ deserialize (context) {
50
+ this.range = context.read()
51
+ this.referencedExports = context.read()
52
+ this.extraOptions = context.read()
53
+ super.deserialize(context)
54
+ }
55
+ }
56
+
57
+ makeSerializable(ImportDependency, '@mpxjs/webpack-plugin/lib/dependencies/ImportDependency')
58
+
59
+ ImportDependency.Template = class ImportDependencyTemplate extends (
60
+ ModuleDependency.Template
61
+ ) {
62
+ /**
63
+ * @param {Dependency} dependency the dependency for which the template should be applied
64
+ * @param {ReplaceSource} source the current replace source which can be modified
65
+ * @param {DependencyTemplateContext} templateContext the context object
66
+ * @returns {void}
67
+ */
68
+ apply (
69
+ dependency,
70
+ source,
71
+ { runtimeTemplate, module, moduleGraph, chunkGraph, runtimeRequirements }
72
+ ) {
73
+ const dep = /** @type {ImportDependency} */ (dependency)
74
+ const block = /** @type {AsyncDependenciesBlock} */ (
75
+ moduleGraph.getParentBlock(dep)
76
+ )
77
+ let content = runtimeTemplate.moduleNamespacePromise({
78
+ chunkGraph,
79
+ block: block,
80
+ module: /** @type {Module} */ (moduleGraph.getModule(dep)),
81
+ request: dep.request,
82
+ strict: /** @type {BuildMeta} */ (module.buildMeta).strictHarmonyModule,
83
+ message: 'import()',
84
+ runtimeRequirements
85
+ })
86
+ // replace fakeType by 9 to fix require.async to commonjs2 module like 'module.exports = function(){...}'
87
+ content = content.replace(/(__webpack_require__\.t\.bind\(.+,\s*)(\d+)(\s*\))/, (_, p1, p2, p3) => {
88
+ return p1 + '9' + p3
89
+ })
90
+
91
+ // require.async 的场景且配置了重试次数才注入 RetryRuntimeModule
92
+ const extraOptions = dep.extraOptions || {}
93
+ if (extraOptions.isRequireAsync && extraOptions.retryRequireAsync && extraOptions.retryRequireAsync.times > 0) {
94
+ runtimeRequirements.add(RetryRuntimeGlobal)
95
+ content = `${RetryRuntimeGlobal}(function() { return ${content} }, ${extraOptions.retryRequireAsync.times}, ${extraOptions.retryRequireAsync.interval})`
96
+ }
97
+
98
+ source.replace(dep.range[0], dep.range[1] - 1, content)
99
+ }
100
+ }
101
+
102
+ module.exports = ImportDependency
@@ -1,7 +1,7 @@
1
1
  const Template = require('webpack/lib/Template')
2
2
  const RuntimeModule = require('webpack/lib/RuntimeModule')
3
3
 
4
- const RetryRuntimeGlobal = '__webpack_require__.__retry'
4
+ const RetryRuntimeGlobal = '__webpack_require__.mpxR'
5
5
 
6
6
  class RetryRuntimeModule extends RuntimeModule {
7
7
  constructor () {
package/lib/index.js CHANGED
@@ -14,8 +14,7 @@ const EntryPlugin = require('webpack/lib/EntryPlugin')
14
14
  const JavascriptModulesPlugin = require('webpack/lib/javascript/JavascriptModulesPlugin')
15
15
  const FlagEntryExportAsUsedPlugin = require('webpack/lib/FlagEntryExportAsUsedPlugin')
16
16
  const FileSystemInfo = require('webpack/lib/FileSystemInfo')
17
- const ImportDependency = require('webpack/lib/dependencies/ImportDependency')
18
- const ImportDependencyTemplate = require('./dependencies/ImportDependencyTemplate')
17
+ const ImportDependency = require('./dependencies/ImportDependency')
19
18
  const AsyncDependenciesBlock = require('webpack/lib/AsyncDependenciesBlock')
20
19
  const ProvidePlugin = require('webpack/lib/ProvidePlugin')
21
20
  const normalize = require('./utils/normalize')
@@ -77,8 +76,10 @@ const VirtualModulesPlugin = require('webpack-virtual-modules')
77
76
  const RuntimeGlobals = require('webpack/lib/RuntimeGlobals')
78
77
  const LoadAsyncChunkModule = require('./react/LoadAsyncChunkModule')
79
78
  const ExternalModule = require('webpack/lib/ExternalModule')
80
- const { RetryRuntimeModule, RetryRuntimeGlobal } = require('./retry-runtime-module')
81
- require('./utils/check-core-version-match')
79
+ const { RetryRuntimeModule, RetryRuntimeGlobal } = require('./dependencies/RetryRuntimeModule')
80
+ const checkVersionCompatibility = require('./utils/check-core-version-match')
81
+
82
+ checkVersionCompatibility()
82
83
 
83
84
  const isProductionLikeMode = options => {
84
85
  return options.mode === 'production' || !options.mode
@@ -696,7 +697,8 @@ class MpxWebpackPlugin {
696
697
  compilation.dependencyFactories.set(RequireExternalDependency, new NullFactory())
697
698
  compilation.dependencyTemplates.set(RequireExternalDependency, new RequireExternalDependency.Template())
698
699
 
699
- compilation.dependencyTemplates.set(ImportDependency, new ImportDependencyTemplate())
700
+ compilation.dependencyFactories.set(ImportDependency, normalModuleFactory)
701
+ compilation.dependencyTemplates.set(ImportDependency, new ImportDependency.Template())
700
702
  })
701
703
 
702
704
  compiler.hooks.thisCompilation.tap('MpxWebpackPlugin', (compilation, { normalModuleFactory }) => {
@@ -784,7 +786,7 @@ class MpxWebpackPlugin {
784
786
  removedChunks: [],
785
787
  forceProxyEventRules: this.options.forceProxyEventRules,
786
788
  // 若配置disableRequireAsync=true, 则全平台构建不支持异步分包
787
- supportRequireAsync: !this.options.disableRequireAsync && (this.options.mode === 'wx' || this.options.mode === 'ali' || this.options.mode === 'tt' || isWeb(this.options.mode) || isReact(this.options.mode)),
789
+ supportRequireAsync: !this.options.disableRequireAsync && (this.options.mode === 'wx' || this.options.mode === 'ali' || this.options.mode === 'tt' || isWeb(this.options.mode) || (isReact(this.options.mode) && this.options.rnConfig.supportSubpackage)),
788
790
  partialCompileRules: this.options.partialCompileRules,
789
791
  collectDynamicEntryInfo: ({ resource, packageName, filename, entryType, hasAsync }) => {
790
792
  const curInfo = mpx.dynamicEntryInfo[packageName] = mpx.dynamicEntryInfo[packageName] || {
@@ -1448,16 +1450,9 @@ class MpxWebpackPlugin {
1448
1450
  // 删除root query
1449
1451
  if (queryObj.root) request = addQuery(request, {}, false, ['root'])
1450
1452
  // wx、ali和web平台支持require.async,其余平台使用CommonJsAsyncDependency进行模拟抹平
1451
- const supportRequireAsync = isReact(mpx.mode)
1452
- ? (mpx.supportRequireAsync && this.options.rnConfig.supportSubpackage)
1453
- : mpx.supportRequireAsync
1454
- if (supportRequireAsync) {
1453
+ if (mpx.supportRequireAsync) {
1455
1454
  if (isWeb(mpx.mode) || isReact(mpx.mode)) {
1456
1455
  if (isReact(mpx.mode)) tarRoot = transSubpackage(mpx.transSubpackageRules, tarRoot)
1457
- request = addQuery(request, {
1458
- isRequireAsync: true,
1459
- retryRequireAsync: JSON.stringify(this.options.retryRequireAsync)
1460
- })
1461
1456
  const depBlock = new AsyncDependenciesBlock(
1462
1457
  {
1463
1458
  name: tarRoot + '/index'
@@ -1465,7 +1460,10 @@ class MpxWebpackPlugin {
1465
1460
  expr.loc,
1466
1461
  request
1467
1462
  )
1468
- const dep = new ImportDependency(request, expr.range)
1463
+ const dep = new ImportDependency(request, expr.range, undefined, {
1464
+ isRequireAsync: true,
1465
+ retryRequireAsync: this.options.retryRequireAsync
1466
+ })
1469
1467
  dep.loc = expr.loc
1470
1468
  depBlock.addDependency(dep)
1471
1469
  parser.state.current.addBlock(depBlock)
@@ -17,6 +17,18 @@ module.exports = function ({ print }) {
17
17
  el.isBuiltIn = true
18
18
  return 'mpx-progress'
19
19
  },
20
+ android (tag, { el }) {
21
+ el.isBuiltIn = true
22
+ return 'mpx-progress'
23
+ },
24
+ harmony (tag, { el }) {
25
+ el.isBuiltIn = true
26
+ return 'mpx-progress'
27
+ },
28
+ ios (tag, { el }) {
29
+ el.isBuiltIn = true
30
+ return 'mpx-progress'
31
+ },
20
32
  props: [
21
33
  {
22
34
  test: /^(border-radius|font-size|color|active-mode|duration)$/,
@@ -8,6 +8,18 @@ module.exports = function ({ print }) {
8
8
  el.isBuiltIn = true
9
9
  return 'mpx-slider'
10
10
  },
11
+ android (tag, { el }) {
12
+ el.isBuiltIn = true
13
+ return 'mpx-slider'
14
+ },
15
+ harmony (tag, { el }) {
16
+ el.isBuiltIn = true
17
+ return 'mpx-slider'
18
+ },
19
+ ios (tag, { el }) {
20
+ el.isBuiltIn = true
21
+ return 'mpx-slider'
22
+ },
11
23
  props: [
12
24
  {
13
25
  test: /^color$/,
@@ -11,7 +11,7 @@ const JD_UNSUPPORTED_TAG_NAME_ARR = ['functional-page-navigator', 'live-pusher',
11
11
  // 快应用不支持的标签集合
12
12
  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']
13
13
  // RN不支持的标签集合
14
- const RN_UNSUPPORTED_TAG_NAME_ARR = ['open-data', 'official-account', 'editor', 'functional-page-navigator', 'live-player', 'live-pusher', 'ad', 'progress', 'slider', 'audio', 'camera', 'match-media', 'page-container', 'editor', 'keyboard-accessory', 'map']
14
+ const RN_UNSUPPORTED_TAG_NAME_ARR = ['open-data', 'official-account', 'editor', 'functional-page-navigator', 'live-player', 'live-pusher', 'ad', 'audio', 'camera', 'match-media', 'page-container', 'editor', 'keyboard-accessory', 'map']
15
15
 
16
16
  /**
17
17
  * @param {function(object): function} print
@@ -463,7 +463,9 @@ module.exports = function getSpec ({ warn, error }) {
463
463
  ali (prefix) {
464
464
  const prefixMap = {
465
465
  bind: 'on',
466
- catch: 'catch'
466
+ catch: 'catch',
467
+ 'capture-catch': 'capture-catch',
468
+ 'capture-bind': 'capture-on'
467
469
  }
468
470
  if (!prefixMap[prefix]) {
469
471
  error(`Ali environment does not support [${prefix}] event handling!`)
@@ -16,6 +16,8 @@ module.exports = class AddEnvPlugin {
16
16
  apply (resolver) {
17
17
  const target = resolver.ensureHook(this.target)
18
18
  const env = this.env
19
+ const envPattern = new RegExp(`\\.${env}(\\.|$)`)
20
+
19
21
  resolver.getHook(this.source).tapAsync('AddEnvPlugin', (request, resolveContext, callback) => {
20
22
  if (request.env) {
21
23
  return callback()
@@ -32,11 +34,22 @@ module.exports = class AddEnvPlugin {
32
34
  }
33
35
  // 当前资源没有后缀名或者路径不符合fileConditionRules规则时,直接返回
34
36
  if (!extname || !matchCondition(resourcePath, this.fileConditionRules)) return callback()
37
+
35
38
  const queryObj = parseQuery(request.query || '?')
36
39
  queryObj.infix = `${queryObj.infix || ''}.${env}`
40
+
41
+ const resourceBasename = path.basename(resourcePath)
42
+ // 如果 infix 与 resourcePath 无法匹配,则认为未命中env
43
+ if (envPattern.test(resourceBasename) && resourceBasename.includes(queryObj.infix)) {
44
+ request.query = stringifyQuery(queryObj)
45
+ request.env = obj.env
46
+ return callback()
47
+ }
48
+
37
49
  obj.query = stringifyQuery(queryObj)
38
50
  obj.path = addInfix(resourcePath, env, extname)
39
51
  obj.relativePath = request.relativePath && addInfix(request.relativePath, env, extname)
52
+
40
53
  resolver.doResolve(target, Object.assign({}, request, obj), 'add env: ' + env, resolveContext, callback)
41
54
  })
42
55
  }
@@ -17,6 +17,9 @@ module.exports = class AddModePlugin {
17
17
  const target = resolver.ensureHook(this.target)
18
18
  const { options = {}, mode } = this
19
19
  const { defaultMode, fileConditionRules, implicitMode } = options
20
+ const modePattern = new RegExp(`\\.${mode}(\\.|$)`)
21
+ const defaultModePattern = new RegExp(`\\.${defaultMode}(\\.|$)`)
22
+
20
23
  resolver.getHook(this.source).tapAsync('AddModePlugin', (request, resolveContext, callback) => {
21
24
  if (request.mode || request.env) {
22
25
  return callback()
@@ -33,13 +36,28 @@ module.exports = class AddModePlugin {
33
36
  }
34
37
  // 当前资源没有后缀名或者路径不符合fileConditionRules规则时,直接返回
35
38
  if (!extname || !matchCondition(resourcePath, fileConditionRules)) return callback()
39
+
36
40
  const queryObj = parseQuery(request.query || '?')
37
41
  const queryInfix = queryObj.infix
38
42
  if (!implicitMode) queryObj.mode = mode
39
43
  queryObj.infix = `${queryInfix || ''}.${mode}`
44
+
45
+ // 如果已经确认是mode后缀的文件,添加query与mode后直接返回
46
+ if (modePattern.test(path.basename(resourcePath))) {
47
+ request.query = stringifyQuery(queryObj)
48
+ request.mode = obj.mode
49
+ return callback()
50
+ } else if (defaultMode && defaultModePattern.test(path.basename(resourcePath))) {
51
+ queryObj.infix = `${queryInfix || ''}.${defaultMode}`
52
+ request.query = stringifyQuery(queryObj)
53
+ request.mode = obj.mode
54
+ return callback()
55
+ }
56
+
40
57
  obj.query = stringifyQuery(queryObj)
41
58
  obj.path = addInfix(resourcePath, mode, extname)
42
59
  obj.relativePath = request.relativePath && addInfix(request.relativePath, mode, extname)
60
+
43
61
  resolver.doResolve(target, Object.assign({}, request, obj), 'add mode: ' + mode, resolveContext, (err, result) => {
44
62
  if (defaultMode && !result) {
45
63
  queryObj.infix = `${queryInfix || ''}.${defaultMode}`
@@ -3,7 +3,8 @@ import { collectDataset } from '@mpxjs/utils';
3
3
  import { omit, extendObject, useNavigation } from './utils';
4
4
  import eventConfigMap from './event.config';
5
5
  const globalEventState = {
6
- needPress: true
6
+ needPress: true,
7
+ identifier: null
7
8
  };
8
9
  const getTouchEvent = (type, event, config) => {
9
10
  const { navigation, propsRef, layoutRef } = config;
@@ -94,36 +95,48 @@ function checkIsNeedPress(e, type, ref) {
94
95
  ref.current.startTimer[type] = null;
95
96
  }
96
97
  }
98
+ function shouldHandleTapEvent(e, eventConfig) {
99
+ const { identifier } = e.nativeEvent.changedTouches[0];
100
+ return eventConfig.tap && globalEventState.identifier === identifier;
101
+ }
97
102
  function handleTouchstart(e, type, eventConfig) {
98
- // 阻止事件被释放放回对象池,导致对象复用 _stoppedEventTypes 状态被保留
99
103
  e.persist();
100
104
  const { innerRef } = eventConfig;
101
- globalEventState.needPress = true;
102
- innerRef.current.mpxPressInfo.detail = {
103
- x: e.nativeEvent.changedTouches[0].pageX,
104
- y: e.nativeEvent.changedTouches[0].pageY
105
- };
105
+ const touch = e.nativeEvent.changedTouches[0];
106
+ const { identifier } = touch;
107
+ const isSingle = e.nativeEvent.touches.length <= 1;
108
+ if (isSingle) {
109
+ // 仅在 touchstart 记录第一个单指触摸点
110
+ globalEventState.identifier = identifier;
111
+ globalEventState.needPress = true;
112
+ innerRef.current.mpxPressInfo.detail = {
113
+ x: touch.pageX,
114
+ y: touch.pageY
115
+ };
116
+ }
106
117
  handleEmitEvent('touchstart', e, type, eventConfig);
107
118
  if (eventConfig.longpress) {
108
- if (e._stoppedEventTypes?.has('longpress')) {
109
- return;
110
- }
111
- if (eventConfig.longpress.hasCatch) {
112
- e._stoppedEventTypes = e._stoppedEventTypes || new Set();
113
- e._stoppedEventTypes.add('longpress');
119
+ // 只有单指触摸时才启动长按定时器
120
+ if (isSingle) {
121
+ if (e._stoppedEventTypes?.has('longpress')) {
122
+ return;
123
+ }
124
+ if (eventConfig.longpress.hasCatch) {
125
+ e._stoppedEventTypes = e._stoppedEventTypes || new Set();
126
+ e._stoppedEventTypes.add('longpress');
127
+ }
128
+ innerRef.current.startTimer[type] && clearTimeout(innerRef.current.startTimer[type]);
129
+ innerRef.current.startTimer[type] = setTimeout(() => {
130
+ globalEventState.needPress = false;
131
+ handleEmitEvent('longpress', e, type, eventConfig);
132
+ }, 350);
114
133
  }
115
- innerRef.current.startTimer[type] && clearTimeout(innerRef.current.startTimer[type]);
116
- innerRef.current.startTimer[type] = setTimeout(() => {
117
- // 只要触发过longpress, 全局就不再触发tap
118
- globalEventState.needPress = false;
119
- handleEmitEvent('longpress', e, type, eventConfig);
120
- }, 350);
121
134
  }
122
135
  }
123
136
  function handleTouchmove(e, type, eventConfig) {
124
137
  const { innerRef } = eventConfig;
125
138
  handleEmitEvent('touchmove', e, type, eventConfig);
126
- if (eventConfig.tap) {
139
+ if (shouldHandleTapEvent(e, eventConfig)) {
127
140
  checkIsNeedPress(e, type, innerRef);
128
141
  }
129
142
  }
@@ -131,7 +144,8 @@ function handleTouchend(e, type, eventConfig) {
131
144
  const { innerRef, disableTap } = eventConfig;
132
145
  handleEmitEvent('touchend', e, type, eventConfig);
133
146
  innerRef.current.startTimer[type] && clearTimeout(innerRef.current.startTimer[type]);
134
- if (eventConfig.tap) {
147
+ // 只有单指触摸结束时才触发 tap
148
+ if (shouldHandleTapEvent(e, eventConfig)) {
135
149
  checkIsNeedPress(e, type, innerRef);
136
150
  if (!globalEventState.needPress || (type === 'bubble' && disableTap) || e._stoppedEventTypes?.has('tap')) {
137
151
  return;
@@ -4,8 +4,8 @@
4
4
  * ✔ out-of-bounds
5
5
  * ✔ x
6
6
  * ✔ y
7
- * damping
8
- * friction
7
+ * damping
8
+ * friction
9
9
  * ✔ disabled
10
10
  * ✘ scale
11
11
  * ✘ scale-min
@@ -24,8 +24,96 @@ import useNodesRef from './useNodesRef';
24
24
  import { MovableAreaContext } from './context';
25
25
  import { useTransformStyle, splitProps, splitStyle, HIDDEN_STYLE, wrapChildren, flatGesture, extendObject, omit, useNavigation, useRunOnJSCallback } from './utils';
26
26
  import { GestureDetector, Gesture } from 'react-native-gesture-handler';
27
- import Animated, { useSharedValue, useAnimatedStyle, withDecay, runOnJS, runOnUI, withSpring } from 'react-native-reanimated';
27
+ import Animated, { useSharedValue, useAnimatedStyle, runOnJS, runOnUI, withTiming, Easing } from 'react-native-reanimated';
28
28
  import { collectDataset, noop } from '@mpxjs/utils';
29
+ // 超出边界处理函数,参考微信小程序的超出边界衰减效果
30
+ const applyBoundaryDecline = (newValue, range) => {
31
+ 'worklet';
32
+ const decline = (distance) => {
33
+ 'worklet';
34
+ return Math.sqrt(Math.abs(distance));
35
+ };
36
+ if (newValue < range[0]) {
37
+ const overDistance = range[0] - newValue;
38
+ return range[0] - decline(overDistance);
39
+ }
40
+ else if (newValue > range[1]) {
41
+ const overDistance = newValue - range[1];
42
+ return range[1] + decline(overDistance);
43
+ }
44
+ return newValue;
45
+ };
46
+ // 参考微信小程序的弹簧阻尼系统实现
47
+ const withWechatSpring = (toValue, dampingParam = 20, callback) => {
48
+ 'worklet';
49
+ // 弹簧参数计算
50
+ const m = 1; // 质量
51
+ const k = 9 * Math.pow(dampingParam, 2) / 40; // 弹簧系数
52
+ const c = dampingParam; // 阻尼系数
53
+ // 判别式:r = c² - 4mk
54
+ const discriminant = c * c - 4 * m * k;
55
+ // 计算动画持续时间和缓动函数
56
+ let duration;
57
+ let easingFunction;
58
+ if (Math.abs(discriminant) < 0.01) {
59
+ // 临界阻尼 (discriminant ≈ 0)
60
+ // 使用cubic-out模拟临界阻尼的平滑过渡
61
+ duration = Math.max(350, Math.min(800, 2000 / dampingParam));
62
+ easingFunction = Easing.out(Easing.cubic);
63
+ }
64
+ else if (discriminant > 0) {
65
+ // 过阻尼 (discriminant > 0)
66
+ // 使用指数缓动模拟过阻尼的缓慢收敛
67
+ duration = Math.max(450, Math.min(1000, 2500 / dampingParam));
68
+ easingFunction = Easing.out(Easing.exp);
69
+ }
70
+ else {
71
+ // 欠阻尼 (discriminant < 0) - 会产生振荡
72
+ // 计算振荡频率和衰减率
73
+ const dampingRatio = c / (2 * Math.sqrt(m * k)); // 阻尼比
74
+ // 根据阻尼比调整动画参数
75
+ if (dampingRatio < 0.7) {
76
+ // 明显振荡
77
+ duration = Math.max(600, Math.min(1200, 3000 / dampingParam));
78
+ // 创建带振荡的贝塞尔曲线
79
+ easingFunction = Easing.bezier(0.175, 0.885, 0.32, 1.275);
80
+ }
81
+ else {
82
+ // 轻微振荡
83
+ duration = Math.max(400, Math.min(800, 2000 / dampingParam));
84
+ easingFunction = Easing.bezier(0.25, 0.46, 0.45, 0.94);
85
+ }
86
+ }
87
+ return withTiming(toValue, {
88
+ duration,
89
+ easing: easingFunction
90
+ }, callback);
91
+ };
92
+ // 参考微信小程序friction的惯性动画
93
+ const withWechatDecay = (velocity, currentPosition, clampRange, frictionValue = 2, callback) => {
94
+ 'worklet';
95
+ // 微信小程序friction算法: delta = -1.5 * v² / a, 其中 a = -f * v / |v|
96
+ // 如果friction小于等于0,设置为默认值2
97
+ const validFriction = frictionValue <= 0 ? 2 : frictionValue;
98
+ const f = 1000 * validFriction;
99
+ const acceleration = velocity !== 0 ? -f * velocity / Math.abs(velocity) : 0;
100
+ const delta = acceleration !== 0 ? (-1.5 * velocity * velocity) / acceleration : 0;
101
+ let finalPosition = currentPosition + delta;
102
+ // 边界限制
103
+ if (finalPosition < clampRange[0]) {
104
+ finalPosition = clampRange[0];
105
+ }
106
+ else if (finalPosition > clampRange[1]) {
107
+ finalPosition = clampRange[1];
108
+ }
109
+ // 计算动画时长
110
+ const distance = Math.abs(finalPosition - currentPosition);
111
+ const duration = Math.min(1500, Math.max(200, distance * 8));
112
+ return withTiming(finalPosition, {
113
+ duration,
114
+ easing: Easing.out(Easing.cubic)
115
+ }, callback);
116
+ };
29
117
  const styles = StyleSheet.create({
30
118
  container: {
31
119
  position: 'absolute',
@@ -41,7 +129,7 @@ const _MovableView = forwardRef((movableViewProps, ref) => {
41
129
  const hasLayoutRef = useRef(false);
42
130
  const propsRef = useRef({});
43
131
  propsRef.current = (props || {});
44
- const { x = 0, y = 0, inertia = false, disabled = false, animation = true, 'out-of-bounds': outOfBounds = false, 'enable-var': enableVar, 'external-var-context': externalVarContext, 'parent-font-size': parentFontSize, 'parent-width': parentWidth, 'parent-height': parentHeight, direction = 'none', 'disable-event-passthrough': disableEventPassthrough = false, 'simultaneous-handlers': originSimultaneousHandlers = [], 'wait-for': waitFor = [], style = {}, changeThrottleTime = 60, bindtouchstart, catchtouchstart, bindhtouchmove, bindvtouchmove, bindtouchmove, catchhtouchmove, catchvtouchmove, catchtouchmove, bindtouchend, catchtouchend, bindchange } = props;
132
+ const { x = 0, y = 0, inertia = false, disabled = false, animation = true, damping = 20, friction = 2, 'out-of-bounds': outOfBounds = false, 'enable-var': enableVar, 'external-var-context': externalVarContext, 'parent-font-size': parentFontSize, 'parent-width': parentWidth, 'parent-height': parentHeight, direction = 'none', 'disable-event-passthrough': disableEventPassthrough = false, 'simultaneous-handlers': originSimultaneousHandlers = [], 'wait-for': waitFor = [], style = {}, changeThrottleTime = 60, bindtouchstart, catchtouchstart, bindhtouchmove, bindvtouchmove, bindtouchmove, catchhtouchmove, catchvtouchmove, catchtouchmove, bindtouchend, catchtouchend, bindchange } = props;
45
133
  const { hasSelfPercent, normalStyle, hasVarDec, varContextRef, setWidth, setHeight } = useTransformStyle(Object.assign({}, style, styles.container), { enableVar, externalVarContext, parentFontSize, parentWidth, parentHeight });
46
134
  const navigation = useNavigation();
47
135
  const prevSimultaneousHandlersRef = useRef(originSimultaneousHandlers || []);
@@ -106,18 +194,12 @@ const _MovableView = forwardRef((movableViewProps, ref) => {
106
194
  const { x: newX, y: newY } = checkBoundaryPosition({ positionX: Number(x), positionY: Number(y) });
107
195
  if (direction === 'horizontal' || direction === 'all') {
108
196
  offsetX.value = animation
109
- ? withSpring(newX, {
110
- duration: 1500,
111
- dampingRatio: 0.8
112
- })
197
+ ? withWechatSpring(newX, damping)
113
198
  : newX;
114
199
  }
115
200
  if (direction === 'vertical' || direction === 'all') {
116
201
  offsetY.value = animation
117
- ? withSpring(newY, {
118
- duration: 1500,
119
- dampingRatio: 0.8
120
- })
202
+ ? withWechatSpring(newY, damping)
121
203
  : newY;
122
204
  }
123
205
  if (bindchange) {
@@ -293,7 +375,7 @@ const _MovableView = forwardRef((movableViewProps, ref) => {
293
375
  triggerEndOnJS
294
376
  });
295
377
  const runOnJSCallback = useRunOnJSCallback(runOnJSCallbackRef);
296
- // 节流版本的 change 事件触发
378
+ // 节流版本的change事件触发
297
379
  const handleTriggerChangeThrottled = useCallback(({ x, y, type }) => {
298
380
  'worklet';
299
381
  const now = Date.now();
@@ -357,7 +439,7 @@ const _MovableView = forwardRef((movableViewProps, ref) => {
357
439
  offsetX.value = x;
358
440
  }
359
441
  else {
360
- offsetX.value = newX;
442
+ offsetX.value = applyBoundaryDecline(newX, draggableXRange.value);
361
443
  }
362
444
  }
363
445
  if (direction === 'vertical' || direction === 'all') {
@@ -367,7 +449,7 @@ const _MovableView = forwardRef((movableViewProps, ref) => {
367
449
  offsetY.value = y;
368
450
  }
369
451
  else {
370
- offsetY.value = newY;
452
+ offsetY.value = applyBoundaryDecline(newY, draggableYRange.value);
371
453
  }
372
454
  }
373
455
  if (bindchange) {
@@ -397,18 +479,12 @@ const _MovableView = forwardRef((movableViewProps, ref) => {
397
479
  if (x !== offsetX.value || y !== offsetY.value) {
398
480
  if (x !== offsetX.value) {
399
481
  offsetX.value = animation
400
- ? withSpring(x, {
401
- duration: 1500,
402
- dampingRatio: 0.8
403
- })
482
+ ? withWechatSpring(x, damping)
404
483
  : x;
405
484
  }
406
485
  if (y !== offsetY.value) {
407
486
  offsetY.value = animation
408
- ? withSpring(y, {
409
- duration: 1500,
410
- dampingRatio: 0.8
411
- })
487
+ ? withWechatSpring(y, damping)
412
488
  : y;
413
489
  }
414
490
  if (bindchange) {
@@ -420,14 +496,10 @@ const _MovableView = forwardRef((movableViewProps, ref) => {
420
496
  }
421
497
  }
422
498
  else if (inertia) {
423
- // 惯性处理
499
+ // 惯性处理 - 使用微信小程序friction算法
424
500
  if (direction === 'horizontal' || direction === 'all') {
425
501
  xInertialMotion.value = true;
426
- offsetX.value = withDecay({
427
- velocity: e.velocityX / 10,
428
- rubberBandEffect: outOfBounds,
429
- clamp: draggableXRange.value
430
- }, () => {
502
+ offsetX.value = withWechatDecay(e.velocityX / 10, offsetX.value, draggableXRange.value, friction, () => {
431
503
  xInertialMotion.value = false;
432
504
  if (bindchange) {
433
505
  runOnJS(runOnJSCallback)('handleTriggerChange', {
@@ -439,11 +511,7 @@ const _MovableView = forwardRef((movableViewProps, ref) => {
439
511
  }
440
512
  if (direction === 'vertical' || direction === 'all') {
441
513
  yInertialMotion.value = true;
442
- offsetY.value = withDecay({
443
- velocity: e.velocityY / 10,
444
- rubberBandEffect: outOfBounds,
445
- clamp: draggableYRange.value
446
- }, () => {
514
+ offsetY.value = withWechatDecay(e.velocityY / 10, offsetY.value, draggableYRange.value, friction, () => {
447
515
  yInertialMotion.value = false;
448
516
  if (bindchange) {
449
517
  runOnJS(runOnJSCallback)('handleTriggerChange', {
@@ -1,5 +1,4 @@
1
- import { useState, useCallback, forwardRef, useImperativeHandle } from 'react';
2
- import { View, StyleSheet } from 'react-native';
1
+ import { useState, useCallback, forwardRef, useImperativeHandle, Fragment } from 'react';
3
2
  const _PortalManager = forwardRef((props, ref) => {
4
3
  const [state, setState] = useState({
5
4
  portals: []
@@ -31,10 +30,9 @@ const _PortalManager = forwardRef((props, ref) => {
31
30
  portals: state.portals
32
31
  }));
33
32
  return (<>
34
- {state.portals.map(({ key, children }, i) => (<View key={key} collapsable={false} // Need collapsable=false here to clip the elevations
35
- style={[StyleSheet.absoluteFill, { zIndex: 1000 + i, pointerEvents: 'box-none' }]}>
33
+ {state.portals.map(({ key, children }) => (<Fragment key={key}>
36
34
  {children}
37
- </View>))}
35
+ </Fragment>))}
38
36
  </>);
39
37
  });
40
38
  export default _PortalManager;