js/pico.js   F
last analyzed

Complexity

Total Complexity 83
Complexity/F 1.89

Size

Lines of Code 718
Function Count 44

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
wmc 83
eloc 288
c 0
b 0
f 0
dl 0
loc 718
rs 2
mnd 39
bc 39
fnc 44
bpm 0.8863
cpm 1.8863
noi 0

How to fix   Complexity   

Complexity

Complex classes like js/pico.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
 * CMS Pico - Create websites using Pico CMS for Nextcloud.
3
 *
4
 * @copyright Copyright (c) 2019, Daniel Rudolf (<[email protected]>)
5
 *
6
 * @license GNU AGPL version 3 or any later version
7
 *
8
 * This program is free software: you can redistribute it and/or modify
9
 * it under the terms of the GNU Affero General Public License as
10
 * published by the Free Software Foundation, either version 3 of the
11
 * License, or (at your option) any later version.
12
 *
13
 * This program is distributed in the hope that it will be useful,
14
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16
 * GNU Affero General Public License for more details.
17
 *
18
 * You should have received a copy of the GNU Affero General Public License
19
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
20
 */
21
22
/** global: OC */
23
/** global: OCA */
24
/** global: jQuery */
25
26
(function (document, $, OC, OCA) {
27
	'use strict';
28
29
	if (!OCA.CMSPico) {
30
		/** @namespace OCA.CMSPico */
31
		OCA.CMSPico = {};
32
	}
33
34
	/**
35
	 * @class
36
	 *
37
	 * @param {jQuery}        $element
38
	 * @param {Object}        [options]
39
	 * @param {string}        [options.route]
40
	 * @param {jQuery|string} [options.template]
41
	 * @param {jQuery|string} [options.loadingTemplate]
42
	 * @param {jQuery|string} [options.errorTemplate]
43
	 */
44
	OCA.CMSPico.List = function ($element, options) {
45
		this.initialize($element, options);
46
	};
47
48
	/**
49
	 * @lends OCA.CMSPico.List.prototype
50
	 */
51
	OCA.CMSPico.List.prototype = {
52
		/** @member {jQuery} */
53
		$element: $(),
54
55
		/** @member {string} */
56
		route: '',
57
58
		/** @member {jQuery} */
59
		$template: $(),
60
61
		/** @member {jQuery} */
62
		$loadingTemplate: $(),
63
64
		/** @member {jQuery} */
65
		$errorTemplate: $(),
66
67
		/**
68
		 * @constructs
69
		 *
70
		 * @param {jQuery}        $element
71
		 * @param {Object}        [options]
72
		 * @param {string}        [options.route]
73
		 * @param {jQuery|string} [options.template]
74
		 * @param {jQuery|string} [options.loadingTemplate]
75
		 * @param {jQuery|string} [options.errorTemplate]
76
		 */
77
		initialize: function ($element, options) {
78
			this.$element = $element;
79
80
			options = $.extend({
81
				route: $element.data('route'),
82
				template: $element.data('template'),
83
				loadingTemplate: $element.data('loadingTemplate'),
84
				errorTemplate: $element.data('errorTemplate')
85
			}, options);
86
87
			this.route = options.route;
88
			this.$template = $(options.template);
89
			this.$loadingTemplate = $(options.loadingTemplate);
90
			this.$errorTemplate = $(options.errorTemplate);
91
92
			var signature = 'OCA.CMSPico.List.initialize()';
93
			if (!this.route) {
94
				throw signature + ': No route given';
95
			}
96
			if (!this.$template.length) {
97
				throw signature + ': No valid list template given';
98
			}
99
			if (!this.$loadingTemplate.length) {
100
				throw signature + ': No valid loading template given';
101
			}
102
			if (!this.$errorTemplate.length) {
103
				throw signature + ': No valid error template given';
104
			}
105
		},
106
107
		/**
108
		 * @public
109
		 */
110
		reload: function () {
111
			this._api('GET');
112
		},
113
114
		/**
115
		 * @public
116
		 * @abstract
117
		 *
118
		 * @param {Object} data
119
		 */
120
		update: function (data) {},
121
122
		/**
123
		 * @protected
124
		 *
125
		 * @param {string}         method
126
		 * @param {string}         [item]
127
		 * @param {Object}         [data]
128
		 * @param {function(data)} [callback]
129
		 */
130
		_api: function (method, item, data, callback) {
131
			var that = this,
132
				url = this.route + (item ? ((this.route.slice(-1) !== '/') ? '/' : '') + item : '');
133
134
			this._content(this.$loadingTemplate);
135
136
			$.ajax({
137
				method: method,
138
				url: OC.generateUrl(url),
139
				data: data || {}
140
			}).done(function (data, textStatus, jqXHR) {
141
				if (callback === undefined) {
142
					that.update(data);
143
				} else if (typeof callback === 'function') {
144
					callback(data);
145
				}
146
			}).fail(function (jqXHR, textStatus, errorThrown) {
147
				that._error(jqXHR.responseJSON);
148
			});
149
		},
150
151
		/**
152
		 * @protected
153
		 *
154
		 * @param {jQuery}  $template
155
		 * @param {object}  [vars]
156
		 * @param {boolean} [replaceContent]
157
		 *
158
		 * @returns {jQuery}
159
		 */
160
		_content: function ($template, vars, replaceContent) {
161
			var $baseElement = $($template.data('replaces') || $template.data('appendTo') || this.$element),
162
				$content = $template.octemplate(vars || {}) || $();
163
164
			$baseElement.find('.has-tooltip').tooltip('hide');
165
			$content.find('.has-tooltip').tooltip();
166
167
			if ((replaceContent !== undefined) ? replaceContent : $template.data('replaces')) {
168
				$baseElement.empty();
169
			}
170
171
			$content.appendTo($baseElement);
172
173
			return $content;
174
		},
175
176
		/**
177
		 * @protected
178
		 *
179
		 * @param {Object}  [responseData]
180
		 * @param {string}  [responseData.error]
181
		 * @param {string}  [responseData.errorField]
182
		 * @param {int}     [responseData.status]
183
		 * @param {string}  [responseData.exception]
184
		 * @param {?string} [responseData.exceptionMessage]
185
		 * @param {?int}    [responseData.exceptionCode]
186
		 */
187
		_error: function (responseData) {
188
			responseData = responseData || {};
189
190
			var $error = this._content(this.$errorTemplate, responseData),
191
				that = this;
192
193
			if (responseData.error) {
194
				$error.find('.error-details').show();
195
			}
196
197
			if (responseData.exception) {
198
				$error.find('.exception-details').show();
199
			}
200
201
			$error.find('.action-reload').on('click.CMSPicoList', function (event) {
202
				that.reload();
203
			});
204
		}
205
	};
206
207
	/**
208
	 * @class
209
	 *
210
	 * @param {jQuery} $element
211
	 * @param {Object} [options]
212
	 * @param {string} [options.route]
213
	 */
214
	OCA.CMSPico.Form = function ($element, options) {
215
		this.initialize($element, options);
216
	};
217
218
	/**
219
	 * @lends OCA.CMSPico.Form.prototype
220
	 */
221
	OCA.CMSPico.Form.prototype = {
222
		/** @member {jQuery} */
223
		$element: $(),
224
225
		/** @member {string} */
226
		route: '',
227
228
		/**
229
		 * @constructs
230
		 *
231
		 * @param {jQuery} $element
232
		 * @param {Object} [options]
233
		 * @param {string} [options.route]
234
		 */
235
		initialize: function ($element, options) {
236
			this.$element = $element;
237
238
			options = $.extend({
239
				route: $element.data('route')
240
			}, options);
241
242
			this.route = options.route;
243
244
			if (!this.route) {
245
				throw 'OCA.CMSPico.Form.initialize(): No route given';
246
			}
247
		},
248
249
		/**
250
		 * @public
251
		 * @abstract
252
		 */
253
		prepare: function () {},
254
255
		/**
256
		 * @public
257
		 * @abstract
258
		 */
259
		submit: function () {},
260
261
		/**
262
		 * @protected
263
		 *
264
		 * @param {jQuery}              $element
265
		 * @param {string|number|Array} [value]
266
		 *
267
		 * @returns {jQuery|string|number|Array}
268
		 */
269
		_val: function ($element, value) {
270
			if (value !== undefined) {
271
				return $element.is(':input') ? $element.val(value) : $element.text(value);
272
			}
273
274
			return $element.is(':input') ? $element.val() : $element.text();
275
		}
276
	};
277
278
	/**
279
	 * @class
280
	 */
281
	OCA.CMSPico.Events = function () {};
282
283
	/**
284
	 * @lends OCA.CMSPico.Events.prototype
285
	 */
286
	OCA.CMSPico.Events.prototype = {
287
		/** @member {?object} */
288
		events: null,
289
290
		/**
291
		 * @public
292
		 *
293
		 * @param {string}   eventName
294
		 * @param {function} callback
295
		 */
296
		on: function (eventName, callback) {
297
			var event = this._parseEventName(eventName);
298
			if (event === false) {
299
				$.error('Invalid event name: ' + eventName);
300
				return;
301
			}
302
303
			if (this.events[event[0]] === undefined) {
304
				this.events[event[0]] = {};
305
			}
306
307
			this.events[event[0]][event[1]] = callback;
308
		},
309
310
		/**
311
		 * @public
312
		 *
313
		 * @param {string} eventName
314
		 */
315
		off: function (eventName) {
316
			var event = this._parseEventName(eventName);
317
			if (event === false) {
318
				$.error('Invalid event name: ' + eventName);
319
				return false;
320
			}
321
322
			if ((this.events[event[0]] !== undefined) && (this.events[event[0]][event[1]] !== undefined)) {
323
				delete this.events[event[0]][event[1]];
324
				return true;
325
			}
326
327
			return false;
328
		},
329
330
		/**
331
		 * @protected
332
		 *
333
		 * @param {string} eventType
334
		 * @param {...*}   eventArguments
335
		 */
336
		_trigger: function (eventType, ...eventArguments) {
337
			if (!this.events[eventType]) {
338
				return;
339
			}
340
341
			var that = this;
342
			$.each(this.events[eventType], function (id, callback) {
343
				callback.apply(that, eventArguments);
344
			});
345
		},
346
347
		/**
348
		 * @protected
349
		 *
350
		 * @param {string} eventName
351
		 *
352
		 * @returns {[string, string]|false}
353
		 */
354
		_parseEventName: function (eventName) {
355
			var pos = eventName.indexOf('.');
356
			pos = (pos !== -1) ? pos : eventName.length;
357
358
			var type = eventName.slice(0, pos),
359
				id = eventName.slice(pos + 1);
360
361
			if (!type || !id) {
362
				return false;
363
			}
364
365
			return [ type, id ];
366
		}
367
	};
368
369
	/**
370
	 * @class
371
	 * @extends OCA.CMSPico.Events
372
	 *
373
	 * @param {jQuery}   $template
374
	 * @param {object}   options
375
	 * @param {string}   options.title
376
	 * @param {object}   [options.templateData]
377
	 * @param {object[]} [options.buttons]
378
	 */
379
	OCA.CMSPico.Dialog = function ($template, options) {
380
		this.initialize($template, options);
381
	};
382
383
	/**
384
	 * @lends OCA.CMSPico.Dialog
385
	 */
386
	$.extend(OCA.CMSPico.Dialog, {
387
		/**
388
		 * @type {number}
389
		 * @constant
390
		 */
391
		BUTTON_ABORT: 1,
392
393
		/**
394
		 * @type {number}
395
		 * @constant
396
		 */
397
		BUTTON_SUBMIT: 2,
398
399
		/**
400
		 * @type {number}
401
		 * @protected
402
		 */
403
		dialogId: 0
404
	});
405
406
	/**
407
	 * @lends OCA.CMSPico.Dialog.prototype
408
	 */
409
	OCA.CMSPico.Dialog.prototype = $.extend({}, OCA.CMSPico.Events.prototype, {
410
		/** @member {jQuery} */
411
		$element: $(),
412
413
		/** @member {string} */
414
		dialogId: '',
415
416
		/** @member {jQuery} */
417
		$template: $(),
418
419
		/** @member {string} */
420
		title: '',
421
422
		/** @member {object} */
423
		templateData: {},
424
425
		/** @member {object[]} */
426
		buttons: [],
427
428
		/** @member {boolean} */
429
		opened: false,
430
431
		/**
432
		 * @constructs
433
		 *
434
		 * @param {jQuery}   $template
435
		 * @param {object}   options
436
		 * @param {string}   options.title
437
		 * @param {object}   [options.templateData]
438
		 * @param {object[]} [options.buttons]
439
		 */
440
		initialize: function ($template, options) {
441
			this.$template = $template;
442
443
			options = $.extend({
444
				title: '',
445
				templateData: {},
446
				buttons: []
447
			}, options);
448
449
			this.title = options.title;
450
			this.templateData = options.templateData;
451
			this.buttons = options.buttons;
452
			this.events = {};
453
454
			this.dialogId = 'picocms-dialog-' + ++OCA.CMSPico.Dialog.dialogId;
455
456
			if (!this.title) {
457
				throw 'OCA.CMSPico.Dialog.initialize(): No dialog title given';
458
			}
459
		},
460
461
		/**
462
		 * @protected
463
		 *
464
		 * @returns {object[]}
465
		 */
466
		_getButtons: function () {
467
			var buttons = [],
468
				that = this;
469
470
			for (var i = 0; i < this.buttons.length; i++) {
471
				if (this.buttons[i].type !== undefined) {
472
					switch (this.buttons[i].type) {
473
						case OCA.CMSPico.Dialog.BUTTON_ABORT:
474
							buttons.push($.extend({
475
								text: t('cms_pico', 'Abort'),
476
								click: function (event) {
477
									that.close();
478
								}
479
							}, this.buttons[i]));
480
							break;
481
482
						case OCA.CMSPico.Dialog.BUTTON_SUBMIT:
483
							buttons.push($.extend({
484
								text: t('cms_pico', 'Save'),
485
								defaultButton: true,
486
								click: function (event) {
487
									that.submit();
488
									that.close();
489
								}
490
							}, this.buttons[i]));
491
							break;
492
					}
493
				} else {
494
					buttons.push(this.buttons[i]);
495
				}
496
			}
497
498
			return buttons;
499
		},
500
501
		/**
502
		 * @public
503
		 */
504
		open: function () {
505
			if (this.opened) {
506
				// nothing to do
507
				return;
508
			}
509
510
			this.$element = this.$template.octemplate($.extend({}, this.templateData, {
511
				id: this.dialogId,
512
				title: this.title
513
			}));
514
515
			$('#app-content').append(this.$element);
516
517
			var that = this;
518
			this.$element.ocdialog({
519
				buttons: this._getButtons(),
520
				close: function () {
521
					that.opened = false;
522
					that.close();
523
				}
524
			});
525
526
			this._trigger('open');
527
			this.opened = true;
528
		},
529
530
		/**
531
		 * @public
532
		 */
533
		submit: function () {
534
			this._trigger('submit');
535
		},
536
537
		/**
538
		 * @public
539
		 */
540
		close: function () {
541
			if (this.opened) {
542
				this.$element.ocdialog('close');
543
				return;
544
			}
545
546
			this._trigger('close');
547
		}
548
	});
549
550
	/**
551
	 * @class
552
	 * @extends OCA.CMSPico.Events
553
	 *
554
	 * @param {jQuery} $element
555
	 * @param {jQuery} $input
556
	 */
557
	OCA.CMSPico.Editable = function ($element, $input) {
558
		this.initialize($element, $input);
559
	};
560
561
	/**
562
	 * @lends OCA.CMSPico.Editable.prototype
563
	 */
564
	OCA.CMSPico.Editable.prototype = $.extend({}, OCA.CMSPico.Events.prototype, {
565
		/** @member {jQuery} */
566
		$element: $(),
567
568
		/** @member {jQuery} */
569
		$input: $(),
570
571
		/** @member {?jQuery} */
572
		$inputIcon: null,
573
574
		/** @member {boolean} */
575
		initialized: false,
576
577
		/** @member {boolean} */
578
		opened: false,
579
580
		/**
581
		 * @constructs
582
		 *
583
		 * @param {jQuery} $element
584
		 * @param {jQuery} $input
585
		 */
586
		initialize: function ($element, $input) {
587
			this.$element = $element;
588
			this.$input = $input;
589
			this.events = {};
590
		},
591
592
		/**
593
		 * @protected
594
		 */
595
		_setupElements: function () {
596
			if (this.initialized) {
597
				return;
598
			}
599
600
			var that = this;
601
602
			this.$inputIcon = $('<span class="input-icon icon-checkmark"></span>')
603
			this.$inputIcon.on('click.CMSPicoEditable', function (event) {
604
				that.submit();
605
				that.close();
606
			});
607
608
			this.$input.on('keyup.CMSPicoEditable', function (event) {
609
				if (event.which === 13) {
610
					that.submit();
611
					that.close();
612
				} else if (event.which === 27) {
613
					that.close();
614
				}
615
			});
616
617
			this.$input
618
				.after(this.$inputIcon)
619
				.addClass('has-input-icon');
620
621
			this.initialized = true;
622
		},
623
624
		/**
625
		 * @public
626
		 */
627
		open: function () {
628
			this._setupElements();
629
630
			this.$element.parent().hide();
631
			this.$input.parent().show();
632
			this.$input.focus();
633
634
			this._trigger('open');
635
			this.opened = true;
636
		},
637
638
		/**
639
		 * @public
640
		 */
641
		submit: function () {
642
			var defaultValue = this.$input.prop('defaultValue'),
643
				value = this.$input.val() || defaultValue;
644
645
			this.$element.text(value);
646
			this.$input.val(value);
647
648
			this._trigger('submit', value, defaultValue);
649
		},
650
651
		/**
652
		 * @public
653
		 */
654
		close: function () {
655
			if (this.opened) {
656
				this.$input.parent().hide();
657
				this.$element.parent().show();
658
659
				this.opened = false;
660
			}
661
662
			this._trigger('close');
663
		},
664
665
		/**
666
		 * @public
667
		 */
668
		toggle: function () {
669
			if (!this.opened) {
670
				this.open();
671
			} else {
672
				this.close();
673
			}
674
		}
675
	});
676
677
	/** @namespace OCA.CMSPico.Util */
678
	OCA.CMSPico.Util = {
679
		/**
680
		 * @param {string} string
681
		 *
682
		 * @returns string
683
		 */
684
		unescape: function (string) {
685
			return string
686
				.replace(/&amp;/g, '&')
687
				.replace(/&lt;/g, '<')
688
				.replace(/&gt;/g, '>')
689
				.replace(/&quot;/g, '"')
690
				.replace(/&#039;/g, "'");
691
		},
692
693
		/**
694
		 * @param {jQuery} $element
695
		 *
696
		 * @returns {object}
697
		 */
698
		serialize: function ($element) {
699
			var dataArray = $element.serializeArray(),
700
				dataObject = {};
701
702
			$element.find('input[type="button"]').each(function () {
703
				var $button = $(this);
704
				dataArray.push({ name: $button.prop('name'), value: $button.val() });
705
			});
706
707
			$.each(dataArray, function (_, data) {
708
				var key = data.name,
709
					matches = key.match(/^([a-z_][a-z0-9_]*)\[(\d*|[a-z0-9_]+)\]/i);
710
711
				if (matches === null) {
712
					dataObject[key] = data.value;
713
				} else {
714
					if (typeof dataObject[matches[1]] !== 'object') {
715
						dataObject[matches[1]] = {};
716
					}
717
718
					var result = dataObject[matches[1]],
719
						subKey = matches[2];
720
721
					key = key.slice(matches[0].length);
722
					matches = key.match(/^\[(\d*|[a-z0-9_]+)\]/i);
723
724
					while (matches !== null) {
725
						if (typeof result[matches[1]] !== 'object') {
726
							result[matches[1]] = {};
727
						}
728
729
						result = result[subKey];
730
						subKey = matches[1];
731
732
						key = key.slice(matches[0].length);
733
						matches = key.match(/^\[(\d*|[a-z0-9_]+)\]/i);
734
					}
735
736
					result[subKey] = data.value;
737
				}
738
			});
739
740
			return dataObject;
741
		}
742
	};
743
})(document, jQuery, OC, OCA);
744