Completed
Pull Request — master (#1036)
by wiese
62:12
created

objectAssign.update   C

Complexity

Conditions 7
Paths 9

Size

Total Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

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