Completed
Push — master ( 3a5d30...efd5c6 )
by Jeroen De
16s
created

skins/cat17/src/app/lib/view_handler/section_info.js   D

Complexity

Total Complexity 61
Complexity/F 2.65

Size

Lines of Code 357
Function Count 23

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 0
wmc 61
c 1
b 1
f 0
nc 1
mnd 3
bc 59
fnc 23
dl 0
loc 357
rs 4.9583
bpm 2.5652
cpm 2.6521
noi 0

18 Functions

Rating   Name   Duplication   Size   Complexity  
A SectionInfo.setIcon 0 19 4
A SectionInfo.getValueIcon 0 3 1
A SectionInfo.setText 0 11 3
A SectionInfo.setLongText 0 9 2
A SectionInfo.setSectionStatus 0 10 3
A section_info.js ➔ createInstance 0 18 1
A SectionInfo.setLongTextIndication 0 6 1
A module.exports.createDonorTypeSectionInfo 0 5 1
A SectionInfo.update 0 3 1
A SectionInfo.defaultBehavior 0 17 4
A module.exports.createPaymentTypeSectionInfo 0 3 1
A module.exports.createFrequencySectionInfo 0 3 1
A section_info.js ➔ createProxy 0 17 1
A module.exports.createMembershipTypeSectionInfo 0 3 1
A module.exports.createAmountFrequencySectionInfo 0 5 1
A SectionInfo.getValueText 0 3 1
A SectionInfo.getValueLongText 0 3 1
B objectAssign.update 0 21 5

How to fix   Complexity   

Complexity

Complex classes like skins/cat17/src/app/lib/view_handler/section_info.js often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
'use strict';
2
3
var objectAssign = require( 'object-assign' ),
4
	_ = require( 'underscore' ),
5
	SECTION_STATUS = {
6
		invalid: 'invalid',
7
		complete: 'complete',
8
		disabled: 'disabled'
9
	},
10
	DOM_SELECTORS = {
11
		data: {
12
			emptyText: 'empty-text',
13
			displayError: 'display-error'
14
		},
15
		classes: {
16
			errorIcon: 'icon-error',
17
			summaryBankInfo: 'bank-info',
18
			sectionInvalid: 'invalid',
19
			sectionComplete: 'completed',
20
			sectionDisabled: 'disabled',
21
			hasLongtext: 'has-longtext',
22
			opened: 'opened'
23
		}
24
	},
25
26
	/**
27
	 * Base class updating mark-up of widgets, repeatedly present in the page, indicating form progress
28
	 *
29
	 * Example:
30
	 * .amount <- set class according to SECTION_STATUS (calculated from validity)
31
	 * |- i <- add class from valueIconMap (or error icon depending on validity)
32
	 * |- span.text <- set text from valueTextMap (with tick for fallback text from data attribute in case of unset value)
33
	 * |- div.info-text-bottom <- set text from valueLongTextMap
34
	 *
35
	 * Not all widgets have to have all features (icon, text, longText), so checks are in place to make widgets flexible
36
	 *
37
	 * In the default set-up it knows how to map a value passed to update to the range of possible values and present it.
38
	 * It does not, by default, set section status as this needs some form of validity.
39
	 */
40
	SectionInfo = {
41
		container: null,
42
		icon: null,
43
		text: null,
44
		longText: null,
45
46
		// mappings between possible form values and content to use
47
		valueIconMap: {},
48
		valueTextMap: {},
49
		valueLongTextMap: {},
50
51
		update: function ( value, validity ) {
52
			this.defaultBehavior( value, validity );
53
		},
54
		/**
55
		 *
56
		 * @param {*} value
57
		 * @param {validation_result} validity
58
		 */
59
		defaultBehavior: function ( value, validity ) {
60
			this.setIcon( this.getValueIcon( value ) );
61
			this.setText( this.getValueText( value ) );
62
			this.setLongText( this.getValueLongText( value ) );
63
64
			if ( validity ) {
65
				if ( validity.dataEntered === false ) {
66
					this.setSectionStatus( SECTION_STATUS.disabled );
67
				} else {
68
					if ( validity.isValid === true ) {
69
						this.setSectionStatus( SECTION_STATUS.complete );
70
					} else {
71
						this.setSectionStatus( SECTION_STATUS.invalid );
72
					}
73
				}
74
			}
75
		},
76
		getValueIcon: function ( value ) {
77
			return this.valueIconMap[ value ];
78
		},
79
		getValueText: function ( value ) {
80
			return this.valueTextMap[ value ];
81
		},
82
		getValueLongText: function ( value ) {
83
			return this.valueLongTextMap[ value ];
84
		},
85
		setText: function ( text ) {
86
			if ( !this.text ) {
87
				return;
88
			}
89
90
			if ( text === undefined ) {
91
				text = this.text.data( DOM_SELECTORS.data.emptyText );
92
			}
93
94
			this.text.text( text );
95
		},
96
		/**
97
		 * Fill the longText element with text. Make sure to call setLongTextIndication if you run your own implementation.
98
		 * @param {string} longText
99
		 */
100
		setLongText: function ( longText ) {
101
			if ( !this.longText ) {
102
				return;
103
			}
104
105
			this.longText.text( longText );
106
107
			this.setLongTextIndication( longText !== '' );
108
		},
109
		/**
110
		 * Indicate that a relevant text is part of this widget (i.e. it's not empty)
111
		 * @param {boolean} hasLongText
112
		 */
113
		setLongTextIndication: function ( hasLongText ) {
114
			this.container.toggleClass( DOM_SELECTORS.classes.hasLongtext, hasLongText );
115
			// collapse possibly opened longtext (see formInfosManager)
116
			this.container.removeClass( DOM_SELECTORS.classes.opened );
117
			this.container.find( '.' + DOM_SELECTORS.classes.opened ).removeClass( DOM_SELECTORS.classes.opened );
118
		},
119
		setIcon: function ( icon ) {
120
			if ( !this.icon ) {
121
				return;
122
			}
123
124
			this.icon.removeClass( DOM_SELECTORS.classes.errorIcon );
125
			this.icon.removeClass( _.values( this.valueIconMap ).join( ' ' ) );
126
127
			if ( icon === undefined ) {
128
				// only configured icon are supposed to communicate validation problems
129
				// @todo Consider always applying the class and decide not to have UI effects in CSS
130
				if ( this.icon.data( DOM_SELECTORS.data.displayError ) === true ) {
131
					this.icon.addClass( DOM_SELECTORS.classes.errorIcon );
132
				}
133
			}
134
			else {
135
				this.icon.addClass( icon );
136
			}
137
		},
138
		setSectionStatus: function ( status ) {
139
			this.container.removeClass( [ DOM_SELECTORS.classes.sectionComplete, DOM_SELECTORS.classes.sectionDisabled, DOM_SELECTORS.classes.sectionInvalid ].join( ' ' ) );
140
			if ( status === 'invalid' ) {
141
				this.container.addClass( DOM_SELECTORS.classes.sectionInvalid );
142
			} else if ( status === 'complete' ) {
143
				this.container.addClass( DOM_SELECTORS.classes.sectionComplete );
144
			} else {
145
				this.container.addClass( DOM_SELECTORS.classes.sectionDisabled );
146
			}
147
		}
148
	},
149
150
	AmountFrequencySectionInfo = objectAssign( Object.create( SectionInfo ), {
151
		// todo Inject actual currency formatter (that knows how to format it depending on locale and incl currency symbol)
152
		currencyFormatter: null,
153
		update: function ( amount, paymentInterval, aggregateValidity ) {
154
			if ( aggregateValidity.isValid === true ) {
155
				this.setSectionStatus( SECTION_STATUS.complete );
156
			} else if ( aggregateValidity.isValid === false ) {
157
				this.setSectionStatus( SECTION_STATUS.invalid );
158
			} else {
159
				this.setSectionStatus( SECTION_STATUS.disabled );
160
			}
161
162
			this.setIcon( this.getValueIcon( paymentInterval ) );
163
164
			if ( this.text ) {
165
				this.setText(
166
					amount === 0 ?
167
						this.text.data( DOM_SELECTORS.data.emptyText ) :
168
						this.currencyFormatter.format( amount ) + ' €'
169
				);
170
			}
171
172
			this.setLongText( this.getValueLongText( paymentInterval ) );
173
		}
174
	} ),
175
176
	PaymentTypeSectionInfo = objectAssign( Object.create( SectionInfo ), {
177
		update: function( paymentType, iban, bic, aggregateValidity ) {
178
			if ( aggregateValidity.isValid === true ) {
179
				this.setSectionStatus( SECTION_STATUS.complete );
180
			} else if ( aggregateValidity.isValid === false ) {
181
				this.setSectionStatus( SECTION_STATUS.invalid );
182
			} else {
183
				this.setSectionStatus( SECTION_STATUS.disabled );
184
			}
185
186
			this.setIcon( this.getValueIcon( paymentType ) );
187
188
			if ( this.text ) {
189
				this.setText(
190
					!aggregateValidity.dataEntered ?
191
						this.text.data( DOM_SELECTORS.data.emptyText ) :
192
						this.getValueText( paymentType )
193
				);
194
			}
195
196
			if ( paymentType !== 'BEZ' ) {
197
				this.setLongText( '' );
198
				return;
199
			}
200
201
			this.setLongText( this.getValueLongText( paymentType ) );
202
203
			if ( this.longText && iban && bic ) {
204
				this.longText.prepend( // intentionally html. Escaping performed through .text() calls on user-input vars
205
					$( '<dl>' ).addClass( DOM_SELECTORS.classes.summaryBankInfo ).append(
206
						$('<dt>').text( 'IBAN' ),
207
						$('<dd>').text( iban ),
208
						$('<dt>').text( 'BIC' ),
209
						$('<dd>').text( bic )
210
					)
211
				);
212
			}
213
		}
214
	} ),
215
216
	DonorTypeSectionInfo = objectAssign( Object.create( SectionInfo ), {
217
		countryNames: null,
218
		update: function( addressType, salutation, title, firstName, lastName, companyName, street, postcode, city, country, email, aggregateValidity ) {
219
			if ( aggregateValidity.isValid === true ) {
220
				this.setSectionStatus( SECTION_STATUS.complete );
221
			} else if ( aggregateValidity.isValid === false ) {
222
				this.setSectionStatus( SECTION_STATUS.invalid );
223
			} else {
224
				this.setSectionStatus( SECTION_STATUS.disabled );
225
			}
226
227
			this.setIcon( this.getValueIcon( addressType ) );
228
229
			if ( this.text ) {
230
				this.setText(
231
					!aggregateValidity.dataEntered ?
232
						this.text.data( DOM_SELECTORS.data.emptyText ) :
233
						this.getValueText( addressType )
234
				);
235
			}
236
237
			if ( !this.longText ) {
238
				return;
239
			}
240
241
			var wrapperTag = '<span>';
242
			var longtext = $( wrapperTag );
243
			// TODO Reuse AddressDisplayHandler maybe?
244
			if ( addressType === 'person' && firstName !== '' && lastName !== '' ) {
245
				longtext.append( $( wrapperTag ).text( salutation + ' ' + title + ' ' + firstName + ' ' + lastName ), '<br>' );
246
			}
247
			else if ( addressType === 'firma' && companyName !== '' ) {
248
				longtext.append( $( wrapperTag ).text( companyName ), '<br>' );
249
			}
250
			if ( street !== '' ) {
251
				longtext.append( $( wrapperTag ).text( street ), '<br>' );
252
			}
253
			if ( postcode !== '' && city !== '' ) {
254
				longtext.append( $( wrapperTag ).text( postcode + ' ' + city ), '<br>' );
255
			}
256
			if ( country !== '' ) {
257
				longtext.append( $( wrapperTag ).text( this.countryNames[ country ] ), '<br>' );
258
			}
259
			if ( email !== '' ) {
260
				longtext.append( $( wrapperTag ).text( email ), '<br>' );
261
			}
262
263
			this.longText.html( longtext );
264
			// we worked around setLongText so have to clean up manually
265
			this.setLongTextIndication( true );
266
		}
267
	} ),
268
269
	/**
270
	 * Create a widget instance with all properties set-up
271
	 *
272
	 * @param {string} type
273
	 * @param {jQuery} widgetNode A HTML node representing a widget
274
	 * @param {object} valueIconMap Mapping of value to icon
275
	 * @param {object} valueTextMap Mapping of value to text
276
	 * @param {object} valueLongTextMap Mapping of value to longText
277
	 * @param {object} additionalDependencies Additional properties that will be merged into the instance of type
278
	 * @return {SectionInfo} or a child
279
	 */
280
	createInstance = function ( type, widgetNode, valueIconMap, valueTextMap, valueLongTextMap, additionalDependencies ) {
281
		return objectAssign(
282
			Object.create( type ),
283
			{
284
				container: widgetNode,
285
286
				// calculate and cache elements
287
				icon: widgetNode.find( 'i:not(".link")' ),
288
				text: widgetNode.find( '.text' ),
289
				longText: widgetNode.find( '.info-detail' ),
290
291
				valueIconMap: valueIconMap,
292
				valueTextMap: valueTextMap,
293
				valueLongTextMap: valueLongTextMap
294
			},
295
			additionalDependencies
296
		);
297
	},
298
299
	/**
300
	 * Proxy that can take DOM `containers` describing widgets, maps them to one widget instance each, forward calls to them
301
	 *
302
	 * We still use jQuery as the selector engine for sub-elements. Possible todo
303
	 *
304
	 * @param type
305
	 * @param {jQuery} containers A list of HTML node representing a widget (matched by the same selector)
306
	 * @param {object} valueIconMap Mapping of value to icon
307
	 * @param {object} valueTextMap Mapping of value to text
308
	 * @param {object} valueLongTextMap Mapping of value to longText
309
	 * @param {object} additionalDependencies Additional properties that will be merged into the instance of type
310
	 * @return {SectionInfo} or a child
311
	 */
312
	createProxy = function ( type, containers, valueIconMap, valueTextMap, valueLongTextMap, additionalDependencies ) {
313
 		var widgets = [];
314
		_.each( containers.get(), function( container ) {
315
			widgets.push( createInstance( type, $( container ), valueIconMap, valueTextMap, valueLongTextMap, additionalDependencies ) );
316
		} );
317
318
		return objectAssign( {
319
			widgets: widgets,
320
			update: function () {
321
				var originalArgs = arguments;
322
				// There is no _.apply unfortunately and _.invoke can't pass `arguments`
323
				_.each( this.widgets, function ( widget ) {
324
					widget.update.apply( widget, originalArgs );
325
				} );
326
			}
327
		} );
328
	}
329
;
330
331
module.exports = {
332
	createInstance: createInstance,
333
	createProxy: createProxy,
334
	SectionInfo: SectionInfo,
335
	AmountFrequencySectionInfo: AmountFrequencySectionInfo,
336
	PaymentTypeSectionInfo: PaymentTypeSectionInfo,
337
	DonorTypeSectionInfo: DonorTypeSectionInfo,
338
	createFrequencySectionInfo: function ( containers, valueIconMap, valueTextMap, valueLongTextMap ) {
339
		return createProxy( SectionInfo, containers, valueIconMap, valueTextMap, valueLongTextMap );
340
	},
341
	createAmountFrequencySectionInfo: function ( containers, valueIconMap, valueTextMap, valueLongTextMap, currencyFormatter ) {
342
		return createProxy( AmountFrequencySectionInfo, containers, valueIconMap, valueTextMap, valueLongTextMap, {
343
			currencyFormatter: currencyFormatter
344
		} );
345
	},
346
	createPaymentTypeSectionInfo: function ( containers, valueIconMap, valueTextMap, valueLongTextMap ) {
347
		return createProxy( PaymentTypeSectionInfo, containers, valueIconMap, valueTextMap, valueLongTextMap );
348
	},
349
	createDonorTypeSectionInfo: function ( containers, valueIconMap, valueTextMap, countryNames ) {
350
		return createProxy( DonorTypeSectionInfo, containers, valueIconMap, valueTextMap, {}, {
351
			countryNames: countryNames
352
		} );
353
	},
354
	createMembershipTypeSectionInfo: function ( containers, valueIconMap, valueTextMap, valueLongTextMap ) {
355
		return createProxy( SectionInfo, containers, valueIconMap, valueTextMap, valueLongTextMap );
356
	}
357
};
358