neo.mjs 10.0.0-beta.3 → 10.0.0-beta.5
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.
- package/.github/RELEASE_NOTES/v10.0.0-beta.4.md +41 -0
- package/ServiceWorker.mjs +2 -2
- package/apps/portal/index.html +1 -1
- package/apps/portal/view/ViewportController.mjs +1 -1
- package/apps/portal/view/home/FooterContainer.mjs +1 -1
- package/apps/portal/view/learn/MainContainerController.mjs +6 -6
- package/examples/button/effect/MainContainer.mjs +207 -0
- package/examples/button/effect/app.mjs +6 -0
- package/examples/button/effect/index.html +11 -0
- package/examples/button/effect/neo-config.json +6 -0
- package/learn/guides/{Collections.md → datahandling/Collections.md} +6 -6
- package/learn/guides/datahandling/Grids.md +621 -0
- package/learn/guides/{Records.md → datahandling/Records.md} +4 -3
- package/learn/guides/{StateProviders.md → datahandling/StateProviders.md} +146 -1
- package/learn/guides/fundamentals/DeclarativeVDOMWithEffects.md +166 -0
- package/learn/guides/fundamentals/ExtendingNeoClasses.md +359 -0
- package/learn/guides/{Layouts.md → uibuildingblocks/Layouts.md} +40 -38
- package/learn/guides/{form_fields → userinteraction/form_fields}/ComboBox.md +3 -3
- package/learn/tree.json +64 -57
- package/package.json +3 -3
- package/src/DefaultConfig.mjs +2 -2
- package/src/Neo.mjs +244 -88
- package/src/button/Effect.mjs +435 -0
- package/src/collection/Base.mjs +35 -3
- package/src/component/Base.mjs +72 -61
- package/src/container/Base.mjs +28 -24
- package/src/controller/Base.mjs +87 -63
- package/src/core/Base.mjs +207 -33
- package/src/core/Compare.mjs +3 -13
- package/src/core/Config.mjs +230 -0
- package/src/core/ConfigSymbols.mjs +3 -0
- package/src/core/Effect.mjs +127 -0
- package/src/core/EffectBatchManager.mjs +68 -0
- package/src/core/EffectManager.mjs +38 -0
- package/src/core/Util.mjs +3 -18
- package/src/data/RecordFactory.mjs +22 -3
- package/src/grid/Container.mjs +8 -4
- package/src/grid/column/Component.mjs +1 -1
- package/src/state/Provider.mjs +343 -452
- package/src/state/createHierarchicalDataProxy.mjs +124 -0
- package/src/tab/header/EffectButton.mjs +75 -0
- package/src/util/Function.mjs +52 -5
- package/src/vdom/Helper.mjs +9 -10
- package/src/vdom/VNode.mjs +1 -1
- package/src/worker/App.mjs +0 -5
- package/test/siesta/siesta.js +32 -0
- package/test/siesta/tests/CollectionBase.mjs +10 -10
- package/test/siesta/tests/VdomHelper.mjs +22 -59
- package/test/siesta/tests/config/AfterSetConfig.mjs +100 -0
- package/test/siesta/tests/config/Basic.mjs +149 -0
- package/test/siesta/tests/config/CircularDependencies.mjs +166 -0
- package/test/siesta/tests/config/CustomFunctions.mjs +69 -0
- package/test/siesta/tests/config/Hierarchy.mjs +94 -0
- package/test/siesta/tests/config/MemoryLeak.mjs +92 -0
- package/test/siesta/tests/config/MultiLevelHierarchy.mjs +85 -0
- package/test/siesta/tests/core/Effect.mjs +131 -0
- package/test/siesta/tests/core/EffectBatching.mjs +322 -0
- package/test/siesta/tests/neo/MixinStaticConfig.mjs +138 -0
- package/test/siesta/tests/state/Provider.mjs +537 -0
- package/test/siesta/tests/state/createHierarchicalDataProxy.mjs +217 -0
- package/learn/guides/ExtendingNeoClasses.md +0 -331
- /package/learn/guides/{Tables.md → datahandling/Tables.md} +0 -0
- /package/learn/guides/{ApplicationBootstrap.md → fundamentals/ApplicationBootstrap.md} +0 -0
- /package/learn/guides/{ConfigSystemDeepDive.md → fundamentals/ConfigSystemDeepDive.md} +0 -0
- /package/learn/guides/{DeclarativeComponentTreesVsImperativeVdom.md → fundamentals/DeclarativeComponentTreesVsImperativeVdom.md} +0 -0
- /package/learn/guides/{InstanceLifecycle.md → fundamentals/InstanceLifecycle.md} +0 -0
- /package/learn/guides/{MainThreadAddons.md → fundamentals/MainThreadAddons.md} +0 -0
- /package/learn/guides/{Mixins.md → specificfeatures/Mixins.md} +0 -0
- /package/learn/guides/{MultiWindow.md → specificfeatures/MultiWindow.md} +0 -0
- /package/learn/guides/{PortalApp.md → specificfeatures/PortalApp.md} +0 -0
- /package/learn/guides/{ComponentsAndContainers.md → uibuildingblocks/ComponentsAndContainers.md} +0 -0
- /package/learn/guides/{CustomComponents.md → uibuildingblocks/CustomComponents.md} +0 -0
- /package/learn/guides/{WorkingWithVDom.md → uibuildingblocks/WorkingWithVDom.md} +0 -0
- /package/learn/guides/{Forms.md → userinteraction/Forms.md} +0 -0
- /package/learn/guides/{events → userinteraction/events}/CustomEvents.md +0 -0
- /package/learn/guides/{events → userinteraction/events}/DomEvents.md +0 -0
package/src/component/Base.mjs
CHANGED
@@ -10,6 +10,7 @@ import Rectangle from '../util/Rectangle.mjs';
|
|
10
10
|
import Style from '../util/Style.mjs';
|
11
11
|
import VDomUtil from '../util/VDom.mjs';
|
12
12
|
import VNodeUtil from '../util/VNode.mjs';
|
13
|
+
import {isDescriptor} from '../core/ConfigSymbols.mjs';
|
13
14
|
|
14
15
|
const
|
15
16
|
addUnits = value => value == null ? value : isNaN(value) ? value : `${value}px`,
|
@@ -53,11 +54,15 @@ class Component extends Base {
|
|
53
54
|
/**
|
54
55
|
* The default alignment specification to position this Component relative to some other
|
55
56
|
* Component, or Element or Rectangle. Only applies in case floating = true.
|
56
|
-
* @member {Object|String} align_={edgeAlign:'t-b',constrainTo:'document.body'}
|
57
|
+
* @member {Object|String} align_={[isDescriptor]: true, merge: 'deep', value: {edgeAlign: 't-b',constrainTo: 'document.body'}}
|
57
58
|
*/
|
58
59
|
align_: {
|
59
|
-
|
60
|
-
|
60
|
+
[isDescriptor]: true,
|
61
|
+
merge : 'deep',
|
62
|
+
value: {
|
63
|
+
edgeAlign : 't-b',
|
64
|
+
constrainTo: 'document.body'
|
65
|
+
}
|
61
66
|
},
|
62
67
|
/**
|
63
68
|
* The name of the App this component belongs to
|
@@ -326,9 +331,13 @@ class Component extends Base {
|
|
326
331
|
stateProvider_: null,
|
327
332
|
/**
|
328
333
|
* Style attributes added to this vdom root. see: getVdomRoot()
|
329
|
-
* @member {Object}
|
334
|
+
* @member {Object} style={[isDescriptor]: true, merge: 'shallow', value: null}
|
330
335
|
*/
|
331
|
-
style_:
|
336
|
+
style_: {
|
337
|
+
[isDescriptor]: true,
|
338
|
+
merge : 'shallow',
|
339
|
+
value : null
|
340
|
+
},
|
332
341
|
/**
|
333
342
|
* You can pass a used theme directly to any component,
|
334
343
|
* to style specific component trees differently from your main view.
|
@@ -375,10 +384,16 @@ class Component extends Base {
|
|
375
384
|
updateDepth_: 1,
|
376
385
|
/**
|
377
386
|
* The component vnode tree. Available after the component got rendered.
|
378
|
-
* @member {Object} vnode_
|
387
|
+
* @member {Object} vnode_=={[isDescriptor]: true, value: null, isEqual: (a, b) => a === b,}
|
379
388
|
* @protected
|
380
389
|
*/
|
381
|
-
vnode_:
|
390
|
+
vnode_: {
|
391
|
+
[isDescriptor]: true,
|
392
|
+
clone : 'none',
|
393
|
+
cloneOnGet : 'none',
|
394
|
+
isEqual : (a, b) => a === b, // vnode trees can be huge, and will get compared by the vdom worker.
|
395
|
+
value : null,
|
396
|
+
},
|
382
397
|
/**
|
383
398
|
* Shortcut for style.width, defaults to px
|
384
399
|
* @member {Number|String|null} width_=null
|
@@ -395,9 +410,13 @@ class Component extends Base {
|
|
395
410
|
wrapperCls_: null,
|
396
411
|
/**
|
397
412
|
* Top level style attributes. Useful in case getVdomRoot() does not point to the top level DOM node.
|
398
|
-
* @member {Object|null} wrapperStyle_=null
|
413
|
+
* @member {Object|null} wrapperStyle_={[isDescriptor]: true, merge: 'shallow', value: null}
|
399
414
|
*/
|
400
|
-
wrapperStyle_:
|
415
|
+
wrapperStyle_: {
|
416
|
+
[isDescriptor]: true,
|
417
|
+
merge : 'shallow',
|
418
|
+
value : null
|
419
|
+
},
|
401
420
|
/**
|
402
421
|
* The vdom markup for this component.
|
403
422
|
* @member {Object} _vdom={}
|
@@ -588,7 +607,12 @@ class Component extends Base {
|
|
588
607
|
afterSetConfig(key, value, oldValue) {
|
589
608
|
let me = this;
|
590
609
|
|
591
|
-
if (
|
610
|
+
if (Neo.isUsingStateProviders && me[twoWayBindingSymbol]) {
|
611
|
+
// When a component config is updated by its state provider, this flag is set to the config's key.
|
612
|
+
// This prevents circular updates in two-way data bindings by skipping the push back to the state provider.
|
613
|
+
if (me._skipTwoWayPush === key) {
|
614
|
+
return;
|
615
|
+
}
|
592
616
|
let binding = me.bind?.[key];
|
593
617
|
|
594
618
|
if (binding?.twoWay) {
|
@@ -646,21 +670,6 @@ class Component extends Base {
|
|
646
670
|
}
|
647
671
|
}
|
648
672
|
|
649
|
-
/**
|
650
|
-
* Triggered after the flex config got changed
|
651
|
-
* @param {Number|String|null} value
|
652
|
-
* @param {Number|String|null} oldValue
|
653
|
-
* @protected
|
654
|
-
*/
|
655
|
-
afterSetFlex(value, oldValue) {
|
656
|
-
if (!isNaN(value)) {
|
657
|
-
value = `${value} ${value} 0%`
|
658
|
-
}
|
659
|
-
|
660
|
-
this.configuredFlex = value;
|
661
|
-
this.changeVdomRootKey('flex', value)
|
662
|
-
}
|
663
|
-
|
664
673
|
/**
|
665
674
|
* Triggered after the hasUnmountedVdomChanges config got changed
|
666
675
|
* @param {Boolean} value
|
@@ -949,6 +958,16 @@ class Component extends Base {
|
|
949
958
|
}
|
950
959
|
}
|
951
960
|
|
961
|
+
/**
|
962
|
+
* Triggered after the stateProvider config got changed
|
963
|
+
* @param {Neo.state.Provider} value
|
964
|
+
* @param {Object|Neo.state.Provider|null} oldValue
|
965
|
+
* @protected
|
966
|
+
*/
|
967
|
+
afterSetStateProvider(value, oldValue) {
|
968
|
+
value?.createBindings(this)
|
969
|
+
}
|
970
|
+
|
952
971
|
/**
|
953
972
|
* Triggered after the style config got changed
|
954
973
|
* @param {Object} value
|
@@ -1242,8 +1261,7 @@ class Component extends Base {
|
|
1242
1261
|
}
|
1243
1262
|
}
|
1244
1263
|
|
1245
|
-
|
1246
|
-
return Neo.merge({}, value, me.constructor.config.align)
|
1264
|
+
return value
|
1247
1265
|
}
|
1248
1266
|
|
1249
1267
|
/**
|
@@ -1803,7 +1821,7 @@ class Component extends Base {
|
|
1803
1821
|
* @returns {Neo.state.Provider|null}
|
1804
1822
|
*/
|
1805
1823
|
getStateProvider(ntype) {
|
1806
|
-
if (!
|
1824
|
+
if (!Neo.isUsingStateProviders) {
|
1807
1825
|
return null
|
1808
1826
|
}
|
1809
1827
|
|
@@ -1940,16 +1958,11 @@ class Component extends Base {
|
|
1940
1958
|
}
|
1941
1959
|
|
1942
1960
|
/**
|
1943
|
-
*
|
1944
|
-
* @param {Object} config
|
1945
|
-
* @param {Boolean} [preventOriginalConfig] True prevents the instance from getting an originalConfig property
|
1961
|
+
* @param args
|
1946
1962
|
*/
|
1947
|
-
initConfig(
|
1948
|
-
super.initConfig(
|
1949
|
-
|
1950
|
-
let me = this;
|
1951
|
-
|
1952
|
-
me.getStateProvider()?.parseConfig(me)
|
1963
|
+
initConfig(...args) {
|
1964
|
+
super.initConfig(...args);
|
1965
|
+
this.getStateProvider()?.createBindings(this)
|
1953
1966
|
}
|
1954
1967
|
|
1955
1968
|
/**
|
@@ -2035,28 +2048,15 @@ class Component extends Base {
|
|
2035
2048
|
* @returns {Object} config
|
2036
2049
|
*/
|
2037
2050
|
mergeConfig(...args) {
|
2038
|
-
let
|
2039
|
-
config
|
2040
|
-
|
2041
|
-
// it should be possible to set custom configs for the vdom on instance level,
|
2042
|
-
// however there will be already added attributes (e.g. id), so a merge seems to be the best strategy.
|
2043
|
-
vdom = {...me._vdom || {}, ...config.vdom || {}};
|
2051
|
+
let config = super.mergeConfig(...args),
|
2052
|
+
vdom = config.vdom || config._vdom || {};
|
2044
2053
|
|
2045
|
-
//
|
2046
|
-
//
|
2047
|
-
|
2048
|
-
|
2049
|
-
if (config.style) {
|
2050
|
-
// If we are passed an object, merge it with the class's own style
|
2051
|
-
me.style = Neo.typeOf(config.style) === 'Object' ? {...config.style, ...me.constructor.config.style} : config.style
|
2052
|
-
}
|
2054
|
+
// It should be possible to modify root level vdom attributes on instance level.
|
2055
|
+
// Note that vdom is not a real config, but implemented via get() & set().
|
2056
|
+
this._vdom = Neo.clone({...vdom, ...this._vdom || {}}, true);
|
2053
2057
|
|
2054
|
-
me.wrapperStyle = Neo.clone(config.wrapperStyle, false);
|
2055
|
-
|
2056
|
-
delete config.style;
|
2057
2058
|
delete config._vdom;
|
2058
2059
|
delete config.vdom;
|
2059
|
-
delete config.wrapperStyle;
|
2060
2060
|
|
2061
2061
|
return config
|
2062
2062
|
}
|
@@ -2141,8 +2141,12 @@ class Component extends Base {
|
|
2141
2141
|
*
|
2142
2142
|
*/
|
2143
2143
|
onConstructed() {
|
2144
|
-
super.onConstructed()
|
2145
|
-
|
2144
|
+
super.onConstructed()
|
2145
|
+
|
2146
|
+
let me = this;
|
2147
|
+
|
2148
|
+
me.keys?.register(me);
|
2149
|
+
me.getStateProvider()?.createBindings(me)
|
2146
2150
|
}
|
2147
2151
|
|
2148
2152
|
/**
|
@@ -2315,10 +2319,12 @@ class Component extends Base {
|
|
2315
2319
|
* @param {Boolean} [mount] Mount the DOM after the vnode got created
|
2316
2320
|
*/
|
2317
2321
|
async render(mount) {
|
2318
|
-
let me
|
2319
|
-
autoMount
|
2320
|
-
{app}
|
2321
|
-
{useVdomWorker} = Neo.config;
|
2322
|
+
let me = this,
|
2323
|
+
autoMount = mount || me.autoMount,
|
2324
|
+
{app} = me,
|
2325
|
+
{unitTestMode, useVdomWorker} = Neo.config;
|
2326
|
+
|
2327
|
+
if (unitTestMode) return;
|
2322
2328
|
|
2323
2329
|
// Verify that the critical rendering path => CSS files for the new tree is in place
|
2324
2330
|
if (autoMount && currentWorker.countLoadingThemeFiles !== 0) {
|
@@ -2632,6 +2638,11 @@ class Component extends Base {
|
|
2632
2638
|
* @protected
|
2633
2639
|
*/
|
2634
2640
|
updateVdom(resolve, reject) {
|
2641
|
+
if (Neo.config.unitTestMode) {
|
2642
|
+
reject?.();
|
2643
|
+
return
|
2644
|
+
}
|
2645
|
+
|
2635
2646
|
let me = this,
|
2636
2647
|
{app, mounted, parentId, vnode} = me;
|
2637
2648
|
|
package/src/container/Base.mjs
CHANGED
@@ -1,12 +1,13 @@
|
|
1
|
-
import Component
|
2
|
-
import LayoutBase
|
3
|
-
import LayoutCard
|
4
|
-
import LayoutFit
|
5
|
-
import LayoutGrid
|
6
|
-
import LayoutHbox
|
7
|
-
import LayoutVBox
|
8
|
-
import Logger
|
9
|
-
import NeoArray
|
1
|
+
import Component from '../component/Base.mjs';
|
2
|
+
import LayoutBase from '../layout/Base.mjs';
|
3
|
+
import LayoutCard from '../layout/Card.mjs';
|
4
|
+
import LayoutFit from '../layout/Fit.mjs';
|
5
|
+
import LayoutGrid from '../layout/Grid.mjs';
|
6
|
+
import LayoutHbox from '../layout/HBox.mjs';
|
7
|
+
import LayoutVBox from '../layout/VBox.mjs';
|
8
|
+
import Logger from '../util/Logger.mjs';
|
9
|
+
import NeoArray from '../util/Array.mjs';
|
10
|
+
import {isDescriptor} from '../core/ConfigSymbols.mjs';
|
10
11
|
|
11
12
|
const byWeight = ({ weight : lhs = 0 }, { weight : rhs = 0 }) => lhs - rhs;
|
12
13
|
|
@@ -31,9 +32,15 @@ class Container extends Component {
|
|
31
32
|
*/
|
32
33
|
baseCls: ['neo-container'],
|
33
34
|
/**
|
34
|
-
*
|
35
|
+
* Default configuration for child items within this container.
|
36
|
+
* This config uses a descriptor to enable deep merging with instance based itemDefaults.
|
37
|
+
* @member {Object} itemDefaults_={[isDescriptor]: true, merge: 'deep', value: null}
|
35
38
|
*/
|
36
|
-
itemDefaults_:
|
39
|
+
itemDefaults_: {
|
40
|
+
[isDescriptor]: true,
|
41
|
+
merge : 'deep',
|
42
|
+
value : null
|
43
|
+
},
|
37
44
|
/**
|
38
45
|
* An array or an object of config objects|instances|modules for each child component
|
39
46
|
* @member {Object[]} items_=[]
|
@@ -85,7 +92,13 @@ class Container extends Component {
|
|
85
92
|
* ]
|
86
93
|
* });
|
87
94
|
*/
|
88
|
-
items_:
|
95
|
+
items_: {
|
96
|
+
[isDescriptor]: true,
|
97
|
+
clone : 'shallow',
|
98
|
+
cloneOnGet : 'none',
|
99
|
+
isEqual : () => false,
|
100
|
+
value : []
|
101
|
+
},
|
89
102
|
/**
|
90
103
|
* It is crucial to define a layout before the container does get rendered.
|
91
104
|
* Meaning: onConstructed() is the latest life-cycle point.
|
@@ -342,10 +355,7 @@ class Container extends Component {
|
|
342
355
|
}
|
343
356
|
|
344
357
|
item.set(config);
|
345
|
-
|
346
|
-
// In case an item got created outside a stateProvider based hierarchy, there might be bindings or string
|
347
|
-
// based listeners which still need to get resolved.
|
348
|
-
item.getStateProvider()?.parseConfig(item);
|
358
|
+
item.getStateProvider()?.createBindings(item);
|
349
359
|
break
|
350
360
|
}
|
351
361
|
|
@@ -617,14 +627,8 @@ class Container extends Component {
|
|
617
627
|
config = super.mergeConfig(...args),
|
618
628
|
ctorItems;
|
619
629
|
|
620
|
-
//
|
621
|
-
//
|
622
|
-
|
623
|
-
if (config.itemDefaults) {
|
624
|
-
me._itemDefaults = Neo.clone(config.itemDefaults, true, true);
|
625
|
-
delete config.itemDefaults
|
626
|
-
}
|
627
|
-
|
630
|
+
// Avoid any interference on prototype level
|
631
|
+
// Does not clone existing Neo instances
|
628
632
|
if (config.items) {
|
629
633
|
ctorItems = me.constructor.config.items;
|
630
634
|
|
package/src/controller/Base.mjs
CHANGED
@@ -2,8 +2,11 @@ import Base from '../core/Base.mjs';
|
|
2
2
|
import HashHistory from '../util/HashHistory.mjs';
|
3
3
|
|
4
4
|
const
|
5
|
-
|
6
|
-
|
5
|
+
regexAmountSlashes = /\//g,
|
6
|
+
// Regex to extract the parameter name from a single route segment (e.g., {*itemId} -> itemId)
|
7
|
+
regexParamNameExtraction = /{(\*|\.\.\.)?([^}]+)}/,
|
8
|
+
// Regex to match route parameters like {paramName}, {*paramName}, or {...paramName}
|
9
|
+
regexRouteParam = /{(\*|\.\.\.)?([^}]+)}/g;
|
7
10
|
|
8
11
|
/**
|
9
12
|
* @class Neo.controller.Base
|
@@ -22,25 +25,33 @@ class Controller extends Base {
|
|
22
25
|
*/
|
23
26
|
ntype: 'controller',
|
24
27
|
/**
|
25
|
-
* If the URL does not contain a hash value when
|
26
|
-
*
|
28
|
+
* If the URL does not contain a hash value when this controller instance is created,
|
29
|
+
* Neo.mjs will automatically set this hash value, ensuring a default route is active.
|
27
30
|
* @member {String|null} defaultHash=null
|
28
31
|
*/
|
29
32
|
defaultHash: null,
|
30
33
|
/**
|
34
|
+
* Specifies the handler method to be invoked when no other defined route matches the URL hash.
|
35
|
+
* This acts as a fallback for unhandled routes.
|
31
36
|
* @member {String|null} defaultRoute=null
|
32
37
|
*/
|
33
38
|
defaultRoute: null,
|
34
39
|
/**
|
40
|
+
* Internal map of compiled regular expressions for each route, used for efficient hash matching.
|
41
|
+
* @protected
|
35
42
|
* @member {Object} handleRoutes={}
|
36
43
|
*/
|
37
44
|
handleRoutes: {},
|
38
45
|
/**
|
46
|
+
* Defines the routing rules for the controller. Keys are route patterns, and values are either
|
47
|
+
* handler method names (String) or objects containing `handler` and optional `preHandler` method names.
|
48
|
+
* Route patterns can include parameters like `{paramName}` and wildcards like `{*paramName}` for nested paths.
|
39
49
|
* @example
|
40
50
|
* routes: {
|
41
51
|
* '/home' : 'handleHomeRoute',
|
42
52
|
* '/users/{userId}' : {handler: 'handleUserRoute', preHandler: 'preHandleUserRoute'},
|
43
53
|
* '/users/{userId}/posts/{postId}': 'handlePostRoute',
|
54
|
+
* '/learn/{*itemId}' : 'onLearnRoute', // Captures nested paths like /learn/gettingstarted/Workspaces
|
44
55
|
* 'default' : 'handleOtherRoutes'
|
45
56
|
* }
|
46
57
|
* @member {Object} routes_={}
|
@@ -49,16 +60,18 @@ class Controller extends Base {
|
|
49
60
|
}
|
50
61
|
|
51
62
|
/**
|
63
|
+
* Creates a new Controller instance and registers its `onHashChange` method
|
64
|
+
* to listen for changes in the browser's URL hash.
|
52
65
|
* @param {Object} config
|
53
66
|
*/
|
54
67
|
construct(config) {
|
55
68
|
super.construct(config);
|
56
|
-
|
57
69
|
HashHistory.on('change', this.onHashChange, this)
|
58
70
|
}
|
59
71
|
|
60
72
|
/**
|
61
|
-
*
|
73
|
+
* Processes the defined routes configuration, compiling route patterns into regular expressions
|
74
|
+
* for efficient matching and sorting them by specificity (more slashes first).
|
62
75
|
* @param {Object} value
|
63
76
|
* @param {Object} oldValue
|
64
77
|
* @protected
|
@@ -78,7 +91,13 @@ class Controller extends Base {
|
|
78
91
|
if (key.toLowerCase() === 'default'){
|
79
92
|
me.defaultRoute = value[key]
|
80
93
|
} else {
|
81
|
-
me.handleRoutes[key] = new RegExp(key.replace(
|
94
|
+
me.handleRoutes[key] = new RegExp(key.replace(regexRouteParam, (match, isWildcard, paramName) => {
|
95
|
+
if (isWildcard || paramName.startsWith('*')) {
|
96
|
+
return '(.*)'
|
97
|
+
} else {
|
98
|
+
return '([\\w-.]+)'
|
99
|
+
}
|
100
|
+
}))
|
82
101
|
}
|
83
102
|
})
|
84
103
|
}
|
@@ -88,21 +107,19 @@ class Controller extends Base {
|
|
88
107
|
*/
|
89
108
|
destroy(...args) {
|
90
109
|
HashHistory.un('change', this.onHashChange, this);
|
91
|
-
|
92
110
|
super.destroy(...args)
|
93
111
|
}
|
94
112
|
|
95
113
|
/**
|
96
|
-
*
|
114
|
+
* @returns {Promise<void>}
|
97
115
|
*/
|
98
|
-
async
|
116
|
+
async initAsync() {
|
117
|
+
await super.initAsync();
|
118
|
+
|
99
119
|
let me = this,
|
100
120
|
{defaultHash, windowId} = me,
|
101
121
|
currentHash = HashHistory.first(windowId);
|
102
122
|
|
103
|
-
// get outside the construction chain => a related cmp & vm has to be constructed too
|
104
|
-
await me.timeout(1);
|
105
|
-
|
106
123
|
if (currentHash) {
|
107
124
|
if (currentHash.windowId === windowId) {
|
108
125
|
await me.onHashChange(currentHash, null)
|
@@ -118,9 +135,10 @@ class Controller extends Base {
|
|
118
135
|
}
|
119
136
|
|
120
137
|
/**
|
121
|
-
*
|
122
|
-
*
|
123
|
-
* @param {Object}
|
138
|
+
* Handles changes in the browser's URL hash. It identifies the most specific matching route
|
139
|
+
* and dispatches the corresponding handler, optionally executing a preHandler first.
|
140
|
+
* @param {Object} value - The new hash history entry.
|
141
|
+
* @param {Object} oldValue - The previous hash history entry.
|
124
142
|
*/
|
125
143
|
async onHashChange(value, oldValue) {
|
126
144
|
// We only want to trigger hash changes for the same browser window (SharedWorker context)
|
@@ -129,63 +147,67 @@ class Controller extends Base {
|
|
129
147
|
}
|
130
148
|
|
131
149
|
let me = this,
|
132
|
-
counter = 0,
|
133
|
-
hasRouteBeenFound = false,
|
134
150
|
{handleRoutes, routes} = me,
|
135
151
|
routeKeys = Object.keys(handleRoutes),
|
136
|
-
|
137
|
-
|
152
|
+
bestMatch = null,
|
153
|
+
bestMatchKey = null,
|
154
|
+
bestMatchParams = null;
|
138
155
|
|
139
|
-
|
140
|
-
key
|
141
|
-
|
142
|
-
preHandler = null;
|
143
|
-
responsePreHandler = null;
|
144
|
-
paramObject = {};
|
145
|
-
result = value.hashString.match(handleRoutes[key]);
|
156
|
+
for (let i = 0; i < routeKeys.length; i++) {
|
157
|
+
const key = routeKeys[i];
|
158
|
+
const result = value.hashString.match(handleRoutes[key]);
|
146
159
|
|
147
160
|
if (result) {
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
161
|
+
const
|
162
|
+
arrayParamIds = key.match(regexRouteParam),
|
163
|
+
arrayParamValues = result.splice(1, result.length - 1),
|
164
|
+
paramObject = {};
|
165
|
+
|
166
|
+
if (arrayParamIds) {
|
167
|
+
for (let j = 0; j < arrayParamIds.length; j++) {
|
168
|
+
const paramMatch = arrayParamIds[j].match(regexParamNameExtraction);
|
169
|
+
|
170
|
+
if (paramMatch) {
|
171
|
+
const paramName = paramMatch[2];
|
172
|
+
paramObject[paramName] = arrayParamValues[j];
|
173
|
+
}
|
174
|
+
}
|
153
175
|
}
|
154
176
|
|
155
|
-
|
156
|
-
|
177
|
+
// Logic to determine the best matching route:
|
178
|
+
// 1. Prioritize routes that match a longer string (more specific match).
|
179
|
+
// 2. If lengths are equal, prioritize routes with more slashes (deeper nesting).
|
180
|
+
if (!bestMatch || (result[0].length > bestMatch[0].length) ||
|
181
|
+
(result[0].length === bestMatch[0].length && (key.match(regexAmountSlashes) || []).length > (bestMatchKey.match(regexAmountSlashes) || []).length)) {
|
182
|
+
bestMatch = result;
|
183
|
+
bestMatchKey = key;
|
184
|
+
bestMatchParams = paramObject;
|
157
185
|
}
|
186
|
+
}
|
187
|
+
}
|
158
188
|
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
responsePreHandler = true
|
164
|
-
} else if (Neo.isObject(route)) {
|
165
|
-
handler = route.handler;
|
166
|
-
preHandler = route.preHandler
|
167
|
-
}
|
189
|
+
if (bestMatch) {
|
190
|
+
const route = routes[bestMatchKey];
|
191
|
+
let handler = null,
|
192
|
+
preHandler = null;
|
168
193
|
|
169
|
-
|
194
|
+
if (Neo.isString(route)) {
|
195
|
+
handler = route
|
196
|
+
} else if (Neo.isObject(route)) {
|
197
|
+
handler = route.handler;
|
198
|
+
preHandler = route.preHandler
|
170
199
|
}
|
171
200
|
|
172
|
-
|
173
|
-
}
|
201
|
+
let responsePreHandler = true;
|
174
202
|
|
175
|
-
// execute
|
176
|
-
if (hasRouteBeenFound) {
|
177
203
|
if (preHandler) {
|
178
|
-
responsePreHandler = await me[preHandler]?.call(me,
|
179
|
-
} else {
|
180
|
-
responsePreHandler = true
|
204
|
+
responsePreHandler = await me[preHandler]?.call(me, bestMatchParams, value, oldValue)
|
181
205
|
}
|
182
206
|
|
183
207
|
if (responsePreHandler) {
|
184
|
-
await me[handler]?.call(me,
|
208
|
+
await me[handler]?.call(me, bestMatchParams, value, oldValue)
|
185
209
|
}
|
186
|
-
}
|
187
|
-
|
188
|
-
if (routeKeys.length > 0 && !hasRouteBeenFound) {
|
210
|
+
} else {
|
189
211
|
if (me.defaultRoute) {
|
190
212
|
me[me.defaultRoute]?.(value, oldValue)
|
191
213
|
} else {
|
@@ -195,22 +217,24 @@ class Controller extends Base {
|
|
195
217
|
}
|
196
218
|
|
197
219
|
/**
|
198
|
-
* Placeholder method
|
199
|
-
*
|
200
|
-
* @param {Object}
|
220
|
+
* Placeholder method invoked when no matching route is found for the current URL hash.
|
221
|
+
* Controllers can override this to implement custom behavior for unhandled routes.
|
222
|
+
* @param {Object} value - The current hash history entry.
|
223
|
+
* @param {Object} oldValue - The previous hash history entry.
|
201
224
|
*/
|
202
225
|
onNoRouteFound(value, oldValue) {
|
203
226
|
|
204
227
|
}
|
205
228
|
|
206
229
|
/**
|
207
|
-
* Internal helper method to sort routes by their
|
208
|
-
*
|
209
|
-
* @param {String}
|
210
|
-
* @
|
230
|
+
* Internal helper method to sort routes by their specificity.
|
231
|
+
* Routes with more slashes are considered more specific and are prioritized.
|
232
|
+
* @param {String} route1 - The first route string to compare.
|
233
|
+
* @param {String} route2 - The second route string to compare.
|
234
|
+
* @returns {Number} A negative value if route1 is more specific, a positive value if route2 is more specific, or 0 if they have equal specificity.
|
211
235
|
*/
|
212
236
|
#sortRoutes(route1, route2) {
|
213
|
-
return (route1.match(
|
237
|
+
return (route1.match(regexAmountSlashes) || []).length - (route2.match(regexAmountSlashes)|| []).length
|
214
238
|
}
|
215
239
|
}
|
216
240
|
|