@@ -11,6 +11,53 @@ export const DEFAULT_VIEW_MODE = ViewMode.ListElement;
1111export const DEFAULT_CONTEXT = Context . Any ;
1212export const DEFAULT_THEME = '*' ;
1313
14+ /**
15+ * A class used to compare two matches and their relevancy to determine which of the two gains priority over the other
16+ *
17+ * "level" represents the index of the first default value that was used to find the match with:
18+ * ViewMode being index 0, Context index 1 and theme index 2. Examples:
19+ * - If a default value was used for context, but not view-mode and theme, the "level" will be 1
20+ * - If a default value was used for view-mode and context, but not for theme, the "level" will be 0
21+ * - If no default value was used for any of the fields, the "level" will be 3
22+ *
23+ * "relevancy" represents the amount of values that didn't require a default value to fall back on. Examples:
24+ * - If a default value was used for theme, but not view-mode and context, the "relevancy" will be 2
25+ * - If a default value was used for view-mode and context, but not for theme, the "relevancy" will be 1
26+ * - If a default value was used for all fields, the "relevancy" will be 0
27+ * - If no default value was used for any of the fields, the "relevancy" will be 3
28+ *
29+ * To determine which of two MatchRelevancies is the most relevant, we compare "level" and "relevancy" in that order.
30+ * If any of the two is higher than the other, that match is most relevant. Examples:
31+ * - { level: 1, relevancy: 1 } is more relevant than { level: 0, relevancy: 2 }
32+ * - { level: 1, relevancy: 1 } is less relevant than { level: 1, relevancy: 2 }
33+ * - { level: 1, relevancy: 1 } is more relevant than { level: 1, relevancy: 0 }
34+ * - { level: 1, relevancy: 1 } is less relevant than { level: 2, relevancy: 0 }
35+ * - { level: 1, relevancy: 1 } is more relevant than null
36+ */
37+ class MatchRelevancy {
38+ constructor ( public match : any ,
39+ public level : number ,
40+ public relevancy : number ) {
41+ }
42+
43+ isMoreRelevantThan ( otherMatch : MatchRelevancy ) : boolean {
44+ if ( hasNoValue ( otherMatch ) ) {
45+ return true ;
46+ }
47+ if ( otherMatch . level > this . level ) {
48+ return false ;
49+ }
50+ if ( otherMatch . level === this . level && otherMatch . relevancy > this . relevancy ) {
51+ return false ;
52+ }
53+ return true ;
54+ }
55+
56+ isLessRelevantThan ( otherMatch : MatchRelevancy ) : boolean {
57+ return ! this . isMoreRelevantThan ( otherMatch ) ;
58+ }
59+ }
60+
1461/**
1562 * Factory to allow us to inject getThemeConfigFor so we can mock it in tests
1663 */
@@ -48,47 +95,70 @@ export function listableObjectComponent(objectType: string | GenericConstructor<
4895
4996/**
5097 * Getter to retrieve the matching listable object component
98+ *
99+ * Looping over the provided types, it'll attempt to find the best match depending on the {@link MatchRelevancy} returned by getMatch()
100+ * The most relevant match between types is kept and eventually returned
101+ *
51102 * @param types The types of which one should match the listable component
52103 * @param viewMode The view mode that should match the components
53104 * @param context The context that should match the components
54105 * @param theme The theme that should match the components
55106 */
56107export function getListableObjectComponent ( types : ( string | GenericConstructor < ListableObject > ) [ ] , viewMode : ViewMode , context : Context = DEFAULT_CONTEXT , theme : string = DEFAULT_THEME ) {
57- let bestMatch ;
58- let bestMatchValue = 0 ;
108+ let currentBestMatch : MatchRelevancy = null ;
59109 for ( const type of types ) {
60110 const typeMap = map . get ( type ) ;
61111 if ( hasValue ( typeMap ) ) {
62- const typeModeMap = typeMap . get ( viewMode ) ;
63- if ( hasValue ( typeModeMap ) ) {
64- const contextMap = typeModeMap . get ( context ) ;
65- if ( hasValue ( contextMap ) ) {
66- const match = resolveTheme ( contextMap , theme ) ;
67- if ( hasValue ( match ) ) {
68- return match ;
69- }
70- if ( bestMatchValue < 3 && hasValue ( contextMap . get ( DEFAULT_THEME ) ) ) {
71- bestMatchValue = 3 ;
72- bestMatch = contextMap . get ( DEFAULT_THEME ) ;
73- }
74- }
75- if ( bestMatchValue < 2 &&
76- hasValue ( typeModeMap . get ( DEFAULT_CONTEXT ) ) &&
77- hasValue ( typeModeMap . get ( DEFAULT_CONTEXT ) . get ( DEFAULT_THEME ) ) ) {
78- bestMatchValue = 2 ;
79- bestMatch = typeModeMap . get ( DEFAULT_CONTEXT ) . get ( DEFAULT_THEME ) ;
80- }
112+ const match = getMatch ( typeMap , [ viewMode , context , theme ] , [ DEFAULT_VIEW_MODE , DEFAULT_CONTEXT , DEFAULT_THEME ] ) ;
113+ if ( hasNoValue ( currentBestMatch ) || currentBestMatch . isLessRelevantThan ( match ) ) {
114+ currentBestMatch = match ;
115+ }
116+ }
117+ }
118+ return hasValue ( currentBestMatch ) ? currentBestMatch . match : null ;
119+ }
120+
121+ /**
122+ * Find an object within a nested map, matching the provided keys as best as possible, falling back on defaults wherever
123+ * needed.
124+ *
125+ * Starting off with a Map, it loops over the provided keys, going deeper into the map until it finds a value
126+ * If at some point, no value is found, it'll attempt to use the default value for that index instead
127+ * If the default value exists, the index is stored in the "level"
128+ * If no default value exists, 1 is added to "relevancy"
129+ * See {@link MatchRelevancy} what these represent
130+ *
131+ * @param typeMap a multi-dimensional map
132+ * @param keys the keys of the multi-dimensional map to loop over. Each key represents a level within the map
133+ * @param defaults the default values to use for each level, in case no value is found for the key at that index
134+ * @returns matchAndLevel a {@link MatchRelevancy} object containing the match and its level of relevancy
135+ */
136+ function getMatch ( typeMap : Map < any , any > , keys : any [ ] , defaults : any [ ] ) : MatchRelevancy {
137+ let currentMap = typeMap ;
138+ let level = - 1 ;
139+ let relevancy = 0 ;
140+ for ( let i = 0 ; i < keys . length ; i ++ ) {
141+ // If we're currently checking the theme, resolve it first to take extended themes into account
142+ let currentMatch = defaults [ i ] === DEFAULT_THEME ? resolveTheme ( currentMap , keys [ i ] ) : currentMap . get ( keys [ i ] ) ;
143+ if ( hasNoValue ( currentMatch ) ) {
144+ currentMatch = currentMap . get ( defaults [ i ] ) ;
145+ if ( level === - 1 ) {
146+ level = i ;
81147 }
82- if ( bestMatchValue < 1 &&
83- hasValue ( typeMap . get ( DEFAULT_VIEW_MODE ) ) &&
84- hasValue ( typeMap . get ( DEFAULT_VIEW_MODE ) . get ( DEFAULT_CONTEXT ) ) &&
85- hasValue ( typeMap . get ( DEFAULT_VIEW_MODE ) . get ( DEFAULT_CONTEXT ) . get ( DEFAULT_THEME ) ) ) {
86- bestMatchValue = 1 ;
87- bestMatch = typeMap . get ( DEFAULT_VIEW_MODE ) . get ( DEFAULT_CONTEXT ) . get ( DEFAULT_THEME ) ;
148+ } else {
149+ relevancy ++ ;
150+ }
151+ if ( hasValue ( currentMatch ) ) {
152+ if ( currentMatch instanceof Map ) {
153+ currentMap = currentMatch as Map < any , any > ;
154+ } else {
155+ return new MatchRelevancy ( currentMatch , level > - 1 ? level : i + 1 , relevancy ) ;
88156 }
157+ } else {
158+ return null ;
89159 }
90160 }
91- return bestMatch ;
161+ return null ;
92162}
93163
94164/**
0 commit comments