src/components/points/admin/assets/js/hooks.js   F
last analyzed

Complexity

Total Complexity 105
Complexity/F 2.28

Size

Lines of Code 674
Function Count 46

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 0
nc 1536
dl 0
loc 674
rs 2.4495
c 0
b 0
f 0
wmc 105
mnd 4
bc 104
fnc 46
bpm 2.2608
cpm 2.2826
noi 2

12 Functions

Rating   Name   Duplication   Size   Complexity  
A WordPointsHooks.closeChooser 0 9 1
B WordPointsHooks.init 0 367 2
A WordPointsHooks.clearHookSelection 0 5 1
B 0 672 1
A WordPointsHooks.resize 0 13 1
B WordPointsHooks.appendTitle 0 26 4
A $(document).ready 0 1 1
B WordPointsHooks.saveOrder 0 25 2
A WordPointsHooks.close 0 7 1
B WordPointsHooks.addHook 0 70 5
A WordPointsHooks.save 0 69 2
A WordPointsHooks.fixLabels 0 11 1

How to fix   Complexity   

Complexity

Complex classes like src/components/points/admin/assets/js/hooks.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
/**
2
 * Points Types hooks UI.
3
 *
4
 * Based on the widgets UI, obviously.
5
 *
6
 * @package WordPoints\Points\Administration
7
 * @since 1.0.0
8
 */
9
10
/* global ajaxurl, isRtl, jQuery */
11
12
/**
13
 * @var object WordPointsHooks
14
 */
15
var WordPointsHooks;
16
17
(function ( $ ) {
18
19
WordPointsHooks = {
20
21
	/**
22
	 * Initialize.
23
	 *
24
	 * @since 1.0.0
25
	 */
26
	init : function() {
27
28
		var rem,
29
			the_id,
30
			self = this,
31
			chooser = $( '.hooks-chooser' ),
32
			selectPointsType = chooser.find( '.hooks-chooser-points-types' ),
33
			points_types = $( 'div.hooks-sortables' ),
34
			isRTL = ( 'undefined' !== typeof isRtl && isRtl ),
35
			margin = ( isRTL ? 'marginRight' : 'marginLeft' );
36
37
		$( '#hooks-right' ).children( '.hooks-holder-wrap' ).children( '.points-type-name' ).click( function () {
38
39
			var $this = $( this ),
40
				parent = $this.parent();
41
42
			if ( parent.hasClass( 'closed' ) ) {
43
44
				parent.removeClass( 'closed' );
45
				$this.siblings( '.hooks-sortables' ).sortable( 'refresh' );
46
47
			} else {
48
49
				parent.addClass( 'closed' );
50
			}
51
		});
52
53
		// Open/close points type on click.
54
		$( '#hooks-left' ).children( '.hooks-holder-wrap' ).children( '.points-type-name' ).click( function () {
55
56
			$( this ).parent().toggleClass( 'closed' );
57
		});
58
59
		// Set the height of the points types.
60
		points_types.each( function () {
61
62
			if ( $( this ).parent().hasClass( 'inactive' ) ) {
63
				return true;
64
			}
65
66
			var h = 50,
67
				H = $( this ).children( '.hook' ).length;
68
69
			h = h + parseInt( H * 48, 10 );
70
			$( this ).css( 'minHeight', h + 'px' );
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
71
		});
72
73
		// Let hooks toggle.
74
		$( document.body ).bind( 'click.hooks-toggle', function ( e ) {
75
76
			var target = $( e.target ),
77
				css = {},
78
				hook,
79
				inside,
80
				w;
81
82
			if ( target.parents( '.hook-top' ).length && ! target.parents( '#available-hooks' ).length ) {
83
84
				hook = target.closest( 'div.hook' );
85
				inside = hook.children( '.hook-inside' );
86
				w = parseInt( hook.find( 'input.hook-width' ).val(), 10 );
87
88
				if ( inside.is( ':hidden' ) ) {
89
90
					if ( w > 250 && inside.closest( 'div.hooks-sortables' ).length ) {
91
92
						css.width = w + 30 + 'px';
93
94
						if ( inside.closest( 'div.hook-liquid-right' ).length ) {
95
							css[ margin ] = 235 - w + 'px';
96
						}
97
98
						hook.css( css );
99
					}
100
101
					WordPointsHooks.fixLabels( hook );
102
					inside.slideDown( 'fast' );
103
104
				} else {
105
106
					inside.slideUp( 'fast', function () {
107
108
						hook.css( { 'width':'', margin:'' } );
109
					});
110
				}
111
112
				e.preventDefault();
113
114
			} else if ( target.hasClass( 'hook-control-save' ) ) {
115
116
				if ( ! target.parent().parent().parent().parent().hasClass( 'wordpoints-points-add-new' ) ) {
117
118
					WordPointsHooks.save( target.closest( 'div.hook' ), 0, 1, 0 );
119
					e.preventDefault();
120
				}
121
122
			} else if ( target.hasClass( 'hook-control-remove' ) ) {
123
124
				WordPointsHooks.save( target.closest( 'div.hook' ), 1, 1, 0 );
125
				e.preventDefault();
126
127
			} else if ( target.hasClass( 'hook-control-close' ) ) {
128
129
				WordPointsHooks.close( target.closest( 'div.hook' ) );
130
				e.preventDefault();
131
			}
132
		});
133
134
		// Append titles to hook names when provided.
135
		points_types.children( '.hook' ).each( function () {
136
137
			WordPointsHooks.appendTitle( this );
138
139
			if ( $( 'p.hook-error', this ).length ) {
140
				$( 'a.hook-action', this ).click();
141
			}
142
		});
143
144
		// Make hooks draggable.
145
		$( '#hook-list' ).children( '.hook' ).draggable({
146
			connectToSortable: 'div.hooks-sortables',
147
			handle: '> .hook-top > .hook-title',
148
			distance: 2,
149
			helper: 'clone',
150
			zIndex: 100,
151
			containment: 'document',
152
			start: function ( event, ui ) {
153
154
				var chooser = $( this ).find( '.hooks-chooser' );
155
156
				ui.helper.find( 'div.hook-description' ).hide();
157
				the_id = this.id;
158
159
				if ( chooser.length ) {
160
					// Hide the chooser and move it out of the hook.
161
					$( '#wpbody-content' ).append( chooser.hide() );
162
					// Delete the cloned chooser from the drag helper.
163
					ui.helper.find( '.hooks-chooser' ).remove();
164
					self.clearHookSelection();
165
				}
166
			},
167
			stop: function () {
168
169
				if ( rem ) {
170
					$( rem ).hide();
171
				}
172
173
				rem = '';
174
			}
175
		});
176
177
		// Make hooks sortable.
178
		points_types.sortable({
179
			placeholder: 'hook-placeholder',
180
			items: '> .hook:not( .points-settings )',
181
			handle: '> .hook-top > .hook-title',
182
			cursor: 'move',
183
			distance: 2,
184
			containment: 'document',
185
			start: function ( event, ui ) {
186
187
				var height, $this = $(this),
188
					$wrap = $this.parent(),
189
					inside = ui.item.children( '.hook-inside' );
190
191
				if ( inside.css( 'display' ) === 'block' ) {
192
					inside.hide();
193
					$( this ).sortable( 'refreshPositions' );
194
				}
195
196
				if ( ! $wrap.hasClass('closed') ) {
197
198
					// Lock all open points types' min-height when starting to drag.
199
					// Prevents jumping when dragging a hook from an open points type to a closed points type below.
200
					height = ( ui.item.hasClass('ui-draggable') ) ? $this.height() : 1 + $this.height();
201
					$this.css( 'min-height', height + 'px' );
202
				}
203
			},
204
			stop: function ( event, ui ) {
205
206
				var addNew, hookNumber, $pointsType, $children, child, item,
207
				$hook = ui.item,
208
				id = the_id;
209
210
				if ( $hook.hasClass('deleting') ) {
211
					self.save( $hook, 1, 0, 1 ); // delete hook
212
					$hook.remove();
213
					return;
214
				}
215
216
				addNew = $hook.find('input.add_new').val();
217
				hookNumber = $hook.find('input.multi_number').val();
218
219
				$hook.attr( 'style', '' ).removeClass( 'ui-draggable' );
220
				the_id = '';
221
222
				if ( addNew ) {
223
					if ( 'multi' === addNew ) {
224
225
						$hook.html(
226
							$hook.html().replace( /<[^<>]+>/g, function( tag ) {
227
								return tag.replace( /__i__|%i%/g, hookNumber );
228
							})
229
						);
230
231
						$hook.attr( 'id', id.replace( '__i__', hookNumber ) );
232
						hookNumber++;
233
234
						$( 'div#' + id ).find( 'input.multi_number' ).val( hookNumber );
235
236
					} else if ( 'single' === addNew ) {
237
238
						$hook.attr( 'id', 'new-' + id );
239
						rem = 'div#' + id;
240
					}
241
242
					self.save( $hook, 0, 0, 1 );
243
					$hook.find('input.add_new').val('');
244
				}
245
246
				$pointsType = $hook.parent();
247
248
				if ( $pointsType.parent().hasClass('closed') ) {
249
250
					$pointsType.parent().removeClass( 'closed' );
251
					$children = $pointsType.children( '.hook' );
252
253
					// Make sure the dropped hook is at the top.
254
					if ( $children.length > 1 ) {
255
256
						child = $children.get(0);
257
						item = $hook.get(0);
258
259
						if ( child.id && item.id && child.id !== item.id ) {
260
							$( child ).before( $hook );
261
						}
262
					}
263
				}
264
265
				if ( addNew ) {
266
					$hook.find( 'a.hook-action' ).trigger('click');
267
				} else {
268
					self.saveOrder( $pointsType.attr('id') );
269
				}
270
			},
271
272
			activate: function() {
273
				$(this).parent().addClass( 'hook-hover' );
274
			},
275
276
			deactivate: function() {
277
				// Remove all min-height added on "start"
278
				$(this).css( 'min-height', '' ).parent().removeClass( 'hook-hover' );
279
			}
280
281
		}).sortable( 'option', 'connectWith', 'div.hooks-sortables' );
282
283
		// Make available hooks droppable.
284
		$( '#available-hooks' ).droppable({
285
			tolerance: 'pointer',
286
			accept: function ( o ) {
287
288
				return $( o ).parent().attr( 'id' ) !== 'hook-list';
289
			},
290
			drop: function ( e, ui ) {
291
292
				ui.draggable.addClass( 'deleting' );
293
				$( '#removing-hook' ).hide().children( 'span' ).html( '' );
294
			},
295
			over: function ( e, ui ) {
296
297
				ui.draggable.addClass( 'deleting' );
298
				$( 'div.hook-placeholder' ).hide();
299
300
				if ( ui.draggable.hasClass( 'ui-sortable-helper' ) ) {
301
					$( '#removing-hook' ).show().children( 'span' )
302
						.html( ui.draggable.find( 'div.hook-title' ).children( 'h4' ).html() );
303
				}
304
			},
305
			out: function ( e, ui ) {
306
307
				ui.draggable.removeClass( 'deleting' );
308
				$( 'div.hook-placeholder' ).show();
309
				$( '#removing-hook' ).hide().children( 'span' ).html( '' );
310
			}
311
		});
312
313
		// Points type chooser.
314
		$( '#hooks-right .hooks-holder-wrap' ).each( function( index, element ) {
315
316
			var $element = $( element ),
317
				name = $element.find( '.points-type-name h3' ).text(),
318
				id = $element.find( '.hooks-sortables' ).attr( 'id' ),
319
				li = $('<li tabindex="0">').text( $.trim( name ) );
320
321
			if ( $element.hasClass( 'new-points-type' ) ) {
322
				return;
323
			}
324
325
			if ( index === 0 ) {
326
				li.addClass( 'hooks-chooser-selected' );
327
			}
328
329
			selectPointsType.append( li );
330
			li.data( 'pointsTypeId', id );
331
		});
332
333
		$( '#available-hooks .hook .hook-title' ).on( 'click.hooks-chooser', function() {
334
335
			var hook = $(this).closest( '.hook' );
336
337
			if ( hook.hasClass( 'hook-in-question' ) || ( $( '#hooks-left' ).hasClass( 'chooser' ) ) ) {
338
339
				self.closeChooser();
340
341
			} else {
342
343
				// Open the chooser.
344
				self.clearHookSelection();
345
				$( '#hooks-left' ).addClass( 'chooser' );
346
347
				hook.addClass( 'hook-in-question' ).children( '.hook-description' ).after( chooser );
348
349
				chooser.slideDown( 300, function() {
350
					selectPointsType.find( '.hooks-chooser-selected' ).focus();
351
				});
352
353
				selectPointsType.find( 'li' ).on( 'focusin.hooks-chooser', function() {
354
					selectPointsType.find( '.hooks-chooser-selected' ).removeClass( 'hooks-chooser-selected wp-ui-highlight' );
355
					$( this ).addClass( 'hooks-chooser-selected wp-ui-highlight' );
356
				});
357
			}
358
		});
359
360
		// Add event handlers.
361
		chooser.on( 'click.hooks-chooser', function( event ) {
362
363
			var $target = $( event.target );
364
365
			if ( $target.hasClass('button-primary') ) {
366
367
				self.addHook( chooser );
368
				self.closeChooser();
369
370
			} else if ( $target.hasClass('button') ) {
371
372
				self.closeChooser();
373
			}
374
375
		}).on( 'keyup.hooks-chooser', function( event ) {
376
377
			if ( event.which === $.ui.keyCode.ENTER ) {
378
379
				if ( ! $( event.target ).hasClass('button-primary') ) {
380
					// Close instead of adding when pressing Enter on the Cancel button
381
					self.closeChooser();
382
				} else {
383
					self.addHook( chooser );
384
					self.closeChooser();
385
				}
386
387
			} else if ( event.which === $.ui.keyCode.ESCAPE ) {
388
389
				self.closeChooser();
390
			}
391
		});
392
	},
393
394
	/**
395
	 * Save hook display order.
396
	 *
397
	 * @since 1.0.0
398
	 */
399
	saveOrder : function ( sb ) {
400
		if ( sb ) {
401
			$( '#' + sb ).closest( 'div.hooks-holder-wrap' ).find( '.spinner:first').addClass( 'is-active' );
402
		}
403
404
		var a = {
405
			action: 'wordpoints-points-hooks-order',
406
			savehooks: $( '#_wpnonce_hooks' ).val(),
407
			points_types: []
408
		};
409
410
		$( 'div.hooks-sortables' ).each( function () {
411
412
			if ( $( this ).sortable ) {
413
				a['points_types[' + $( this ).attr( 'id' ) + ']'] = $( this ).sortable( 'toArray' ).join( ',' );
414
			}
415
		});
416
417
		$.post( ajaxurl, a, function() {
418
419
			$( '.spinner ').removeClass( 'is-active' );
420
		});
421
422
		this.resize();
423
	},
424
425
	/**
426
	 * Save hook settings.
427
	 *
428
	 * @since 1.0.0
429
	 */
430
	save : function ( hook, del, animate, order ) {
431
		var sb = hook.closest( 'div.hooks-sortables' ).attr( 'id' ),
432
			data = hook.find( 'form' ).serialize(),
433
			a;
434
435
		hook = $( hook );
436
		$( '.spinner', hook ).addClass( 'is-active' );
437
438
		a = {
439
			action: 'save-wordpoints-points-hook',
440
			savehooks: $( '#_wpnonce_hooks' ).val(),
441
			points_type: sb
442
		};
443
444
		if ( del ) {
445
			a.delete_hook = 1;
446
		}
447
448
		data += '&' + $.param( a );
449
450
		$.post( ajaxurl, data, function ( r ) {
451
452
			var id;
453
454
			if ( del ) {
455
456
				if ( ! $( 'input.hook_number', hook ).val() ) {
457
458
					id = $( 'input.hook-id', hook ).val();
459
					$( '#available-hooks' ).find( 'input.hook-id' ).each( function () {
460
461
						if ( $( this ).val() === id ) {
462
							$( this ).closest( 'div.hook' ).show();
463
						}
464
					});
465
				}
466
467
				if ( animate ) {
468
469
					order = 0;
470
					hook.slideUp( 'fast', function () {
471
472
						$( this ).remove();
473
						WordPointsHooks.saveOrder();
474
					});
475
476
				} else {
477
478
					hook.remove();
479
					WordPointsHooks.resize();
480
				}
481
482
			} else {
483
484
				$( '.spinner' ).removeClass( 'is-active' );
485
486
				if ( r && r.length > 2 ) {
487
488
					$( 'div.hook-content', hook ).html( r );
489
					WordPointsHooks.appendTitle( hook );
490
					WordPointsHooks.fixLabels( hook );
491
				}
492
			}
493
494
			if ( order ) {
495
				WordPointsHooks.saveOrder();
496
			}
497
		});
498
	},
499
500
	/**
501
	 * Append the main setting value to the hook title bar.
502
	 *
503
	 * @since 1.0.0
504
	 *
505
	 * @param {object} hook - The DOM object for the hook whose title to append.
506
	 */
507
	appendTitle : function ( hook ) {
508
509
		var title, $title_append;
510
511
		$title_append = $( '.wordpoints-append-to-hook-title', hook );
512
513
		if ( $title_append.length === 0 ) {
514
515
			// Back-compat.
516
			title = $( 'input[id*="-title"]', hook ).val();
517
518
			if ( ! title ) {
519
				return;
520
			}
521
522
		} else {
523
524
			if ( $title_append.is( 'select' ) ) {
525
				title = $title_append.find( ':selected' ).text();
526
			} else {
527
				title = $title_append.val();
528
			}
529
		}
530
531
		$( '.in-hook-title', hook ).text( ': ' + title );
532
	},
533
534
	/**
535
	 * Resize the hook box.
536
	 *
537
	 * @since 1.0.0
538
	 */
539
	resize : function () {
540
541
		$( 'div.hooks-sortables' ).each( function () {
542
543
			if ( $( this ).parent().hasClass( 'inactive' ) ) {
544
				return true;
545
			}
546
547
			var h = 50, H = $( this ).children( '.hook' ).length;
548
			h = h + parseInt( H * 48, 10 );
549
			$( this ).css( 'minHeight', h + 'px' );
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
550
		});
551
	},
552
553
	/**
554
	 * Fix label element 'for' attributes.
555
	 *
556
	 * @since 1.0.0
557
	 */
558
	fixLabels : function ( hook ) {
559
560
		hook.children( '.hook-inside' ).find( 'label' ).each( function () {
561
562
			var f = $( this ).attr( 'for' );
563
564
			if ( f && f === $( 'input', this ).attr( 'id' ) ) {
565
				$( this ).removeAttr( 'for' );
566
			}
567
		});
568
	},
569
570
	/**
571
	 * Close the hook box.
572
	 *
573
	 * @since 1.0.0
574
	 */
575
	close : function ( hook ) {
576
577
		hook.children( '.hook-inside' ).slideUp( 'fast', function () {
578
579
			hook.css( { 'width':'', margin:'' } );
580
		});
581
	},
582
583
	/**
584
	 * Add a hook via the chooser.
585
	 *
586
	 * @since 1.1.0
587
	 */
588
	addHook: function( chooser ) {
589
590
		var hook,
591
			hookId,
592
			add,
593
			n,
594
			pointsTypeId = chooser.find( '.hooks-chooser-selected' ).data( 'pointsTypeId' ),
595
			pointsType = $( '#' + pointsTypeId );
596
597
		hook = $( '#available-hooks' ).find( '.hook-in-question' ).clone();
598
		hookId = hook.attr('id');
599
		add = hook.find( 'input.add_new' ).val();
600
		n = hook.find( 'input.multi_number' ).val();
601
602
		// Remove the cloned chooser from the hook.
603
		hook.find('.hooks-chooser').remove();
604
605
		if ( 'multi' === add ) {
606
607
			hook.html(
608
				hook.html().replace( /<[^<>]+>/g, function( m ) {
609
					return m.replace( /__i__|%i%/g, n );
610
				})
611
			);
612
613
			hook.attr( 'id', hookId.replace( '__i__', n ) );
614
			n++;
615
			$( '#' + hookId ).find( 'input.multi_number' ).val(n);
616
617
		} else if ( 'single' === add ) {
618
619
			hook.attr( 'id', 'new-' + hookId );
620
			$( '#' + hookId ).hide();
621
		}
622
623
		// Open the hooks container
624
		pointsType.closest( '.hooks-holder-wrap' ).removeClass( 'closed' );
625
		pointsType.sortable( 'refresh' );
626
		pointsType.find( '.points-hooks-settings-separator' ).after( hook );
627
628
		WordPointsHooks.save( hook, 0, 0, 1 );
629
630
		// No longer "new" hook.
631
		hook.find( 'input.add_new' ).val( '' );
632
633
		/*
634
		 * Check if any part of the sidebar is visible in the viewport. If it is, don't scroll.
635
		 * Otherwise, scroll up to so the sidebar is in view.
636
		 *
637
		 * We do this by comparing the top and bottom, of the sidebar so see if they are within
638
		 * the bounds of the viewport.
639
		 */
640
		var viewport_top = $(window).scrollTop(),
641
			viewport_bottom = viewport_top + $(window).height(),
642
			pointsTypeBounds = pointsType.offset();
643
644
		pointsTypeBounds.bottom = pointsTypeBounds.top + pointsType.outerHeight();
645
646
		if ( viewport_top > pointsTypeBounds.bottom || viewport_bottom < pointsTypeBounds.top ) {
647
			$( 'html, body' ).animate({
648
				scrollTop: pointsType.offset().top - 130
649
			}, 200 );
650
		}
651
652
		window.setTimeout( function() {
653
			// Cannot use a callback in the animation above as it fires twice,
654
			// have to queue this "by hand".
655
			hook.find( '.hook-title' ).trigger( 'click' );
656
		}, 250 );
657
	},
658
659
	/**
660
	 * Close the points type chooser.
661
	 *
662
	 * @since 1.1.0
663
	 */
664
	closeChooser: function() {
665
666
		var self = this;
667
668
		$( '.hooks-chooser' ).slideUp( 200, function() {
669
			$( '#wpbody-content' ).append( this );
670
			self.clearHookSelection();
671
		});
672
	},
673
674
	/**
675
	 * Clear the hook selection.
676
	 *
677
	 * @since 1.1.0
678
	 */
679
	clearHookSelection: function() {
680
681
		$( '#hooks-left' ).removeClass( 'chooser' );
682
		$( '.hook-in-question' ).removeClass( 'hook-in-question' );
683
	}
684
};
685
686
$( document ).ready( function() { WordPointsHooks.init(); } );
687
688
})(jQuery);
689