Completed
Push — 14.2 ( 7153cd...f2fc75 )
by Nathan
28:08
created

et2_widget_date.js ➔ set_value   C

Complexity

Conditions 11
Paths 10

Size

Total Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 11
c 1
b 0
f 0
nc 10
nop 1
dl 0
loc 28
rs 5.2653

How to fix   Complexity   

Complexity

Complex classes like et2_widget_date.js ➔ set_value 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
 * EGroupware eTemplate2 - JS Date object
3
 *
4
 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
5
 * @package etemplate
6
 * @subpackage api
7
 * @link http://www.egroupware.org
8
 * @author Nathan Gray
9
 * @copyright Nathan Gray 2011
10
 * @version $Id$
11
 */
12
13
"use strict";
14
15
/*egw:uses
16
	jquery.jquery;
17
	jquery.jquery-ui;
18
	lib/date;
19
	et2_core_inputWidget;
20
	et2_core_valueWidget;
21
*/
22
23
/**
24
 * Class which implements the "date" XET-Tag
25
 *
26
 * Dates are passed to the server in ISO8601 format ("Y-m-d\TH:i:sP"), and data_format is
27
 * handled server-side.
28
 *
29
 * @augments et2_inputWidget
30
 */
31
var et2_date = et2_inputWidget.extend(
32
{
33
	attributes: {
34
		"value": {
35
			"type": "any"
36
		},
37
		"type": {
38
			"ignore": false
39
		},
40
		"blur": {
41
			"name": "Placeholder",
42
			"type": "string",
43
			"default": "",
44
			"description": "This text get displayed if an input-field is empty and does not have the input-focus (blur). It can be used to show a default value or a kind of help-text."
45
		},
46
		"data_format": {
47
			"ignore": true,
48
			"description": "Date/Time format. Can be set as an options to date widget",
49
			"default": ''
50
		},
51
		year_range: {
52
			name: "Year range",
53
			type: "string",
54
			default: "c-10:c+10",
55
			description: "The range of years displayed in the year drop-down: either relative to today's year (\"-nn:+nn\"), relative to the currently selected year (\"c-nn:c+nn\"), absolute (\"nnnn:nnnn\"), or combinations of these formats (\"nnnn:-nn\"). Note that this option only affects what appears in the drop-down, to restrict which dates may be selected use the min and/or max options."
56
		},
57
		min: {
58
			"name": "Minimum",
59
			"type": "any",
60
			"default": et2_no_init,
61
			"description": 'Minimum allowed date.  Multiple types supported:\
62
Date: A date object containing the minimum date.\
63
Number: A number of days from today. For example 2 represents two days from today and -1 represents yesterday.\
64
String: A string in the user\'s date format, or a relative date. Relative dates must contain value and period pairs; valid periods are "y" for years, "m" for months, "w" for weeks, and "d" for days. For example, "+1m +7d" represents one month and seven days from today.'
65
		},
66
		max: {
67
			"name": "Maximum",
68
			"type": "any",
69
			"default": et2_no_init,
70
			"description": 'Maximum allowed date.   Multiple types supported:\
71
Date: A date object containing the maximum date.\
72
Number: A number of days from today. For example 2 represents two days from today and -1 represents yesterday.\
73
String: A string in the user\'s date format, or a relative date. Relative dates must contain value and period pairs; valid periods are "y" for years, "m" for months, "w" for weeks, and "d" for days. For example, "+1m +7d" represents one month and seven days from today.'
74
		},
75
	},
76
77
	legacyOptions: ["data_format"],
78
79
	/**
80
	 * Constructor
81
	 *
82
	 * @memberOf et2_date
83
	 */
84
	init: function() {
85
		this._super.apply(this, arguments);
86
87
		this.date = new Date();
88
		this.date.setUTCHours(0);
89
		this.date.setMinutes(0);
90
		this.date.setSeconds(0);
91
		this.input = null;
92
93
		this.createInputWidget();
94
	},
95
96
	createInputWidget: function() {
97
98
		this.span = $j(document.createElement("span")).addClass("et2_date");
99
100
		this.input_date = $j(document.createElement("input"));
101
		if (this.options.blur) this.input_date.attr('placeholder', this.egw().lang(this.options.blur));
102
		this.input_date.addClass("et2_date").attr("type", "text")
103
			.attr("size", 7)	// strlen("10:00pm")=7
104
			.appendTo(this.span);
105
106
		this.setDOMNode(this.span[0]);
107
108
		// jQuery-UI date picker
109
		if(this._type != 'date-timeonly')
110
		{
111
			this.egw().calendar(this.input_date, this._type == "date-time");
112
		}
113
		else
114
		{
115
			this.input_date.addClass("et2_time");
116
			this.egw().time(this.input_date);
117
		}
118
119
		// Avoid collision of datepicker dialog with input field
120
		this.input_date.datepicker('option', 'beforeShow', function(input, inst){
121
			var cal = inst.dpDiv;
122
			setTimeout(function () {
123
				var $input = jQuery(input);
124
				var inputOffset = $input.offset();
125
				// position the datepicker in freespace zone
126
				// avoid datepicker calendar collision with input field
127
				if (cal.height() + inputOffset.top > window.innerHeight)
128
				{
129
					cal.position({
130
						my: "left center",
131
						at: 'right bottom',
132
						collision: 'flip fit',
133
						of: input,
134
					});
135
				}
136
				
137
			},0);
138
		});
139
		
140
		// Update internal value when changed
141
		var self = this;
142
		this.input_date.bind('change', function(e){
143
			self.set_value(this.value);
144
			return false;
145
		});
146
147
		// Framewok skips nulls, but null needs to be processed here
148
		if(this.options.value == null)
149
		{
150
			this.set_value(null);
151
		}
152
	},
153
154
	set_type: function(_type) {
155
		if(_type != this._type)
156
		{
157
			this._type = _type;
158
			this.createInputWidget();
159
		}
160
	},
161
162
	/**
163
	 * Dynamic disable or enable datepicker
164
	 *
165
	 * @param {boolean} _ro
166
	 */
167
	set_readonly: function(_ro)
168
	{
169
		if (this.input_date && !this.input_date.attr('disabled') != !_ro)
170
		{
171
			this.input_date.attr('disabled', !!_ro)
172
				.datepicker('option', 'disabled', !!_ro);
173
		}
174
	},
175
176
	/**
177
	 * Set (full) year of current date
178
	 *
179
	 * @param {number} _value 4-digit year
180
	 */
181
	set_year: function(_value)
182
	{
183
		this.date.setUTCFullYear(_value);
184
		this.set_value(this.date);
185
	},
186
	/**
187
	 * Set month (1..12) of current date
188
	 *
189
	 * @param {number} _value 1..12
190
	 */
191
	set_month: function(_value)
192
	{
193
		this.date.setUTCMonth(_value-1);
194
		this.set_value(this.date);
195
	},
196
	/**
197
	 * Set day of current date
198
	 *
199
	 * @param {number} _value 1..31
200
	 */
201
	set_date: function(_value)
202
	{
203
		this.date.setUTCDate(_value);
204
		this.set_value(this.date);
205
	},
206
	/**
207
	 * Set hour (0..23) of current date
208
	 *
209
	 * @param {number} _value 0..23
210
	 */
211
	set_hours: function(_value)
212
	{
213
		this.date.setUTCHours(_value);
214
		this.set_value(this.date);
215
	},
216
	/**
217
	 * Set minute (0..59) of current date
218
	 *
219
	 * @param {number} _value 0..59
220
	 */
221
	set_minutes: function(_value)
222
	{
223
		this.date.setUTCMinutes(_value);
224
		this.set_value(this.date);
225
	},
226
	/**
227
	 * Get (full) year of current date
228
	 *
229
	 * @return {number|null} 4-digit year or null for empty
230
	 */
231
	get_year: function()
232
	{
233
		return this.input_date.val() == "" ? null : this.date.getUTCFullYear();
234
	},
235
	/**
236
	 * Get month (1..12) of current date
237
	 *
238
	 * @return {number|null} 1..12 or null for empty
239
	 */
240
	get_month: function()
241
	{
242
		return this.input_date.val() == "" ? null : this.date.getUTCMonth()+1;
243
	},
244
	/**
245
	 * Get day of current date
246
	 *
247
	 * @return {number|null} 1..31 or null for empty
248
	 */
249
	get_date: function()
250
	{
251
		return this.input_date.val() == "" ? null : this.date.getUTCDate();
252
	},
253
	/**
254
	 * Get hour (0..23) of current date
255
	 *
256
	 * @return {number|null} 0..23 or null for empty
257
	 */
258
	get_hours: function()
259
	{
260
		return this.input_date.val() == "" ? null : this.date.getUTCHours();
261
	},
262
	/**
263
	 * Get minute (0..59) of current date
264
	 *
265
	 * @return {number|null} 0..59 or null for empty
266
	 */
267
	get_minutes: function()
268
	{
269
		return this.input_date.val() == "" ? null : this.date.getUTCMinutes();
270
	},
271
	/**
272
	 * Get timestamp
273
	 *
274
	 * You can use set_value to set a timestamp.
275
	 *
276
	 * @return {number|null} timestamp (seconds since 1970-01-01)
277
	 */
278
	get_time: function()
279
	{
280
		return this.input_date.val() == "" ? null : this.date.getTime();
281
	},
282
283
	/**
284
	 * The range of years displayed in the year drop-down: either relative
285
	 * to today's year ("-nn:+nn"), relative to the currently selected year
286
	 * ("c-nn:c+nn"), absolute ("nnnn:nnnn"), or combinations of these formats
287
	 * ("nnnn:-nn"). Note that this option only affects what appears in the
288
	 * drop-down, to restrict which dates may be selected use the min_date
289
	 * and/or max_date options.
290
	 * @param {string} _value
291
	 */
292
	set_year_range: function(_value)
293
	{
294
		if(this.input_date && this._type == 'date')
295
		{
296
			this.input_date.datepicker('option','yearRange',_value);
297
		}
298
		this.options.year_range = _value;
299
	},
300
301
	/**
302
	 * Set the minimum allowed date
303
	 *
304
	 * The minimum selectable date. When set to null, there is no minimum.
305
	 *	Multiple types supported:
306
	 *	Date: A date object containing the minimum date.
307
	 *	Number: A number of days from today. For example 2 represents two days
308
	 *		from today and -1 represents yesterday.
309
	 *	String: A string in the format defined by the dateFormat option, or a
310
	 *		relative date. Relative dates must contain value and period pairs;
311
	 *		valid periods are "y" for years, "m" for months, "w" for weeks, and
312
	 *		"d" for days. For example, "+1m +7d" represents one month and seven
313
	 *		days from today.
314
	 * @param {Date|Number|String} _value
315
	 */
316
	set_min: function(_value) {
317
		if(this.input_date)
318
		{
319
			if (this.is_mobile)
320
			{
321
				this.input_date.attr('min', this._relativeDate(_value));
322
			}
323
			else
324
			{
325
				// Check for full timestamp
326
				if(typeof _value == 'string' && _value.match(/(\d{4})-(\d{2})-(\d{2})T(\d{2})\:(\d{2})\:(\d{2})(?:\.\d{3})?(?:Z|[+-](\d{2})\:(\d{2}))/))
327
				{
328
					_value = new Date(_value);
329
					// Add timezone offset back in, or formatDate will lose those hours
330
					var formatDate = new Date(_value.valueOf() + this.date.getTimezoneOffset() * 60 * 1000);
331
					if(this._type == 'date')
332
					{
333
						_value = jQuery.datepicker.formatDate(this.input_date.datepicker('option', 'dateFormat'), formatDate);
334
					}
335
				}
336
				this.input_date.datepicker('option','minDate',_value);
337
			}
338
		}
339
		this.options.min = _value;
340
	},
341
342
	/**
343
	 * Set the maximum allowed date
344
	 *
345
	 * The maximum selectable date. When set to null, there is no maximum.
346
	 *	Multiple types supported:
347
	 *	Date: A date object containing the maximum date.
348
	 *	Number: A number of days from today. For example 2 represents two days
349
	 *		from today and -1 represents yesterday.
350
	 *	String: A string in the format defined by the dateFormat option, or a
351
	 *		relative date. Relative dates must contain value and period pairs;
352
	 *		valid periods are "y" for years, "m" for months, "w" for weeks, and
353
	 *		"d" for days. For example, "+1m +7d" represents one month and seven
354
	 *		days from today.
355
	 * @param {Date|Number|String} _value
356
	 */
357
	set_max: function(_value) {
358
		if(this.input_date)
359
		{
360
			if (this.is_mobile)
361
			{
362
				this.input_date.attr('max', this._relativeDate(_value));
363
			}
364
			else
365
			{
366
				// Check for full timestamp
367
				if(typeof _value == 'string' && _value.match(/(\d{4})-(\d{2})-(\d{2})T(\d{2})\:(\d{2})\:(\d{2})(?:\.\d{3})?(?:Z|[+-](\d{2})\:(\d{2}))/))
368
				{
369
					_value = new Date(_value);
370
					// Add timezone offset back in, or formatDate will lose those hours
371
					var formatDate = new Date(_value.valueOf() + this.date.getTimezoneOffset() * 60 * 1000);
372
					if(this._type == 'date')
373
					{
374
						_value = jQuery.datepicker.formatDate(this.input_date.datepicker('option', 'dateFormat'), formatDate);
375
					}
376
				}
377
				this.input_date.datepicker('option','maxDate',_value);
378
			}
379
		}
380
		this.options.max = _value;
381
	},
382
	
383
	/**
384
	 * Setting date
385
	 *
386
	 * @param {string|number|Date} _value supported are the following formats:
387
	 * - Date object with usertime as UTC value
388
	 * - string like Date.toJSON()
389
	 * - string or number with timestamp in usertime like server-side uses it
390
	 * - string starting with + or - to add/substract given number of seconds from current value, "+600" to add 10 minutes
391
	 */
392
	set_value: function(_value) {
393
		var old_value = this._oldValue;
394
		if(_value === null || _value === "" || _value === undefined ||
395
			// allow 0 as empty-value for date and date-time widgets, as that is used a lot eg. in InfoLog
396
			_value == 0 && (this._type == 'date-time' || this._type == 'date'))
397
		{
398
			if(this.input_date)
399
			{
400
				this.input_date.val("");
401
			}
402
			if(this._oldValue !== et2_no_init && old_value !== _value)
403
			{
404
				this.change(this.input_date);
405
			}
406
			this._oldValue = _value;
407
			return;
408
		}
409
410
		// timestamp in usertime, convert to 'Y-m-d\\TH:i:s\\Z', as we do on server-side with equivalent of PHP date()
411
		if (typeof _value == 'number' || typeof _value == 'string' && !isNaN(_value) && _value[0] != '+' && _value[0] != '-')
412
		{
413
			_value = date('Y-m-d\\TH:i:s\\Z', _value);
414
		}
415
		// Check for full timestamp
416
		if(typeof _value == 'string' && _value.match(/(\d{4})-(\d{2})-(\d{2})T(\d{2})\:(\d{2})\:(\d{2})(?:\.\d{3})?(?:Z|[+-](\d{2})\:(\d{2}))/))
417
		{
418
			_value = new Date(_value);
419
		}
420
		// Handle just time as a string in the form H:i
421
		if(typeof _value == 'string' && isNaN(_value))
422
		{
423
			try {
424
				// silently fix skiped minutes or times with just one digit, as parser is quite pedantic ;-)
425
				var fix_reg = new RegExp((this._type == "date-timeonly"?'^':' ')+'([0-9]+)(:[0-9]*)?( ?(a|p)m?)?$','i');
426
				var matches = _value.match(fix_reg);
427
				if (matches && (matches[1].length < 2 || matches[2] === undefined || matches[2].length < 3 ||
428
					matches[3] && matches[3] != 'am' && matches[3] != 'pm'))
429
				{
430
					if (matches[1].length < 2 && !matches[3]) matches[1] = '0'+matches[1];
431
					if (matches[2] === undefined) matches[2] = ':00';
432
					while (matches[2].length < 3) matches[2] = ':0'+matches[2].substr(1);
433
					_value = _value.replace(fix_reg, (this._type == "date-timeonly"?'':' ')+matches[1]+matches[2]+matches[3]);
434
					if (matches[4] !== undefined) matches[3] = matches[4].toLowerCase() == 'a' ? 'am' : 'pm';
435
				}
436
				switch(this._type)
437
				{
438
					case "date-timeonly":
439
						var parsed = jQuery.datepicker.parseTime(this.input_date.datepicker('option', 'timeFormat'), _value);
440
						if (!parsed)	// parseTime returns false
441
						{
442
							this.set_validation_error(this.egw().lang("'%1' has an invalid format !!!",_value));
443
							return;
444
						}
445
						this.set_validation_error(false);
446
						// this.date is on current date, changing it in get_value() to 1970-01-01, gives a time-difference, if we are currently on DST
447
						this.date.setDate(1);
448
						this.date.setMonth(0);
449
						this.date.setFullYear(1970);
450
						// Avoid javascript timezone offset, hour is in 'user time'
451
						this.date.setUTCHours(parsed.hour);
452
						this.date.setMinutes(parsed.minute);
453
						if(this.input_date.val() != _value)
454
						{
455
							this.input_date.val(_value);
456
							this.input_date.timepicker('setTime',_value);
457
							if (this._oldValue !== et2_no_init)
458
							{
459
								this.change(this.input_date);
460
							}
461
						}
462
						this._oldValue = this.date.toJSON();
463
						return;
464
					default:
465
						// Parse customfields's date with storage data_format to date object
466
						// Or generally any date widgets with fixed date/time format
467
						if (this.id.match(/^#/g) && this.options.value == _value || (this.options.data_format && this.options.value == _value))
468
						{
469
							switch (this._type)
0 ignored issues
show
Coding Style introduced by
As per coding-style, switch statements should have a default case.
Loading history...
470
							{
471
								case 'date':
472
									var parsed = jQuery.datepicker.parseDate(this.egw().dateTimeFormat(this.options.data_format), _value);
473
									break;
474
								case 'date-time':
475
									var DTformat = this.options.data_format.split(' ');
476
									var parsed = jQuery.datepicker.parseDateTime(this.egw().dateTimeFormat(DTformat[0]),this.egw().dateTimeFormat(DTformat[1]), _value);
477
							}
478
						}
479
						else  // Parse other date widgets date with timepicker date/time format to date onject
480
						{
481
							var parsed = jQuery.datepicker.parseDateTime(this.input_date.datepicker('option', 'dateFormat'),
482
									this.input_date.datepicker('option', 'timeFormat'), _value);
483
							if(!parsed)
484
							{
485
								this.set_validation_error(this.egw().lang("%1' han an invalid format !!!",_value));
486
								return;
487
							}
488
						}
489
						// Update local variable, but remove the timezone offset that
490
						// javascript adds when we parse
491
						if(parsed)
492
						{
493
							this.date = new Date(parsed.valueOf() - parsed.getTimezoneOffset() * 60000);
494
						}
495
496
						this.set_validation_error(false);
497
				}
498
			}
499
			// catch exception from unparsable date and display it empty instead
500
			catch(e) {
501
				return this.set_value(null);
502
			}
503
		} else if (typeof _value == 'object' && _value.date) {
504
			this.date = _value.date;
505
		} else if (typeof _value == 'object' && _value.valueOf) {
506
			this.date = _value;
507
		} else
508
		// string starting with + or - --> add/substract number of seconds from current value
509
		{
510
			this.date.setTime(this.date.getTime()+1000*parseInt(_value));
511
		}
512
513
		// Update input - popups do, but framework doesn't
514
		_value = '';
515
		// Add timezone offset back in, or formatDate will lose those hours
516
		var formatDate = new Date(this.date.valueOf() + this.date.getTimezoneOffset() * 60 * 1000);
517
		if(this._type != 'date-timeonly')
518
		{
519
			_value = jQuery.datepicker.formatDate(this.input_date.datepicker("option","dateFormat"),
520
				formatDate
521
			);
522
		}
523
		if(this._type != 'date')
524
		{
525
			if(this._type != 'date-timeonly') _value += ' ';
526
527
			_value += jQuery.datepicker.formatTime(this.input_date.datepicker("option","timeFormat"),{
528
				hour: formatDate.getHours(),
529
				minute: formatDate.getMinutes(),
530
				seconds: 0,
531
				timezone: 0
532
			});
533
		}
534
		this.input_date.val(_value);
535
		if(this._oldValue !== et2_no_init && old_value != this.getValue())
536
		{
537
			this.change(this.input_date);
538
		}
539
		this._oldValue = _value;
540
	},
541
542
	getValue: function() {
543
		if(this.input_date.val() == "")
544
		{
545
			// User blanked the box
546
			return null;
547
		}
548
		// date-timeonly returns just the seconds, without any date!
549
		if (this._type == 'date-timeonly')
550
		{
551
			this.date.setDate(1);
552
			this.date.setMonth(0);
553
			this.date.setFullYear(1970);
554
		}
555
		else if (this._type == 'date')
556
		{
557
			this.date.setUTCHours(0);
558
			this.date.setUTCMinutes(0);
559
		}
560
561
		// Convert to timestamp - no seconds
562
		this.date.setSeconds(0,0);
563
		return (this.date && typeof this.date.toJSON != 'undefined' && this.date.toJSON())?this.date.toJSON().replace(/\.\d{3}Z$/, 'Z'):this.date;
564
	}
565
});
566
et2_register_widget(et2_date, ["date", "date-time", "date-timeonly"]);
567
568
/**
569
 * @augments et2_date
570
 */
571
var et2_date_duration = et2_date.extend(
572
{
573
	attributes: {
574
		"data_format": {
575
			"name": "Data format",
576
			"default": "m",
577
			"type": "string",
578
			"description": "Units to read/store the data.  'd' = days (float), 'h' = hours (float), 'm' = minutes (int)."
579
		},
580
		"display_format": {
581
			"name": "Display format",
582
			"default": "dhm",
583
			"type": "string",
584
			"description": "Permitted units for displaying the data.  'd' = days, 'h' = hours, 'm' = minutes.  Use combinations to give a choice.  Default is 'dh' = days or hours with selectbox."
585
		},
586
		"percent_allowed": {
587
			"name": "Percent allowed",
588
			"default": false,
589
			"type": "boolean",
590
			"description": "Allows to enter a percentage."
591
		},
592
		"hours_per_day": {
593
			"name": "Hours per day",
594
			"default": 8,
595
			"type": "integer",
596
			"description": "Number of hours in a day, for converting between hours and (working) days."
597
		},
598
		"empty_not_0": {
599
			"name": "0 or empty",
600
			"default": false,
601
			"type": "boolean",
602
			"description": "Should the widget differ between 0 and empty, which get then returned as NULL"
603
		},
604
		"short_labels": {
605
			"name": "Short labels",
606
			"default": false,
607
			"type": "boolean",
608
			"description": "use d/h/m instead of day/hour/minute"
609
		}
610
	},
611
612
	legacyOptions: ["data_format","display_format", "hours_per_day", "empty_not_0", "short_labels"],
613
614
	time_formats: {"d":"d","h":"h","m":"m"},
615
616
	/**
617
	 * Constructor
618
	 *
619
	 * @memberOf et2_date_duration
620
	 */
621
	init: function() {
622
		this._super.apply(this, arguments);
623
624
		this.input = null;
625
626
		// Legacy option put percent in with display format
627
		if(this.options.display_format.indexOf("%") != -1)
628
		{
629
			this.options.percent_allowed = true;
630
			this.options.display_format = this.options.display_format.replace("%","");
631
		}
632
633
		// Clean formats
634
		this.options.display_format = this.options.display_format.replace(/[^dhm]/,'');
635
		if(!this.options.display_format)
636
		{
637
			this.options.display_format = this.attributes.display_format["default"];
638
		}
639
640
		// Get translations
641
		this.time_formats = {
642
			"d": this.options.short_labels ? this.egw().lang("d") : this.egw().lang("Days"),
643
			"h": this.options.short_labels ? this.egw().lang("h") : this.egw().lang("Hours"),
644
			"m": this.options.short_labels ? this.egw().lang("m") : this.egw().lang("Minutes")
645
		},
646
		this.createInputWidget();
647
	},
648
649
	createInputWidget: function() {
650
		// Create nodes
651
		this.node = $j(document.createElement("span"))
652
						.addClass('et2_date_duration');
653
		this.duration = $j(document.createElement("input"))
654
						.addClass('et2_date_duration')
655
						.attr({type: 'number', size: 3});
656
		this.node.append(this.duration);
657
658
		if(this.options.display_format.length > 1)
659
		{
660
			this.format = $j(document.createElement("select"))
661
							.addClass('et2_date_duration');
662
			this.node.append(this.format);
663
664
			for(var i = 0; i < this.options.display_format.length; i++) {
665
				this.format.append("<option value='"+this.options.display_format[i]+"'>"+this.time_formats[this.options.display_format[i]]+"</option>");
666
			}
667
		}
668
		else if (this.time_formats[this.options.display_format])
669
		{
670
			this.format = $j("<span>"+this.time_formats[this.options.display_format]+"</span>").appendTo(this.node);
671
		}
672
		else
673
		{
674
			this.format = $j("<span>"+this.time_formats["m"]+"</span>").appendTo(this.node);
675
		}
676
	},
677
	attachToDOM: function() {
678
		var node = this.getInputNode();
679
		if (node)
680
		{
681
			$j(node).bind("change.et2_inputWidget", this, function(e) {
682
				e.data.change(this);
683
			});
684
		}
685
		et2_DOMWidget.prototype.attachToDOM.apply(this, arguments);
686
	},
687
	getDOMNode: function() {
688
		return this.node[0];
689
	},
690
	getInputNode: function() {
691
		return this.duration[0];
692
	},
693
694
	/**
695
	 * Use id on node, same as DOMWidget
696
	 *
697
	 * @param {string} _value id to set
698
	 */
699
	set_id: function(_value) {
700
		this.id = _value;
701
702
		var node = this.getDOMNode(this);
703
		if (node)
704
		{
705
			if (_value != "")
706
			{
707
				node.setAttribute("id", this.getInstanceManager().uniqueId+'_'+this.id);
708
			}
709
			else
710
			{
711
				node.removeAttribute("id");
712
			}
713
		}
714
	},
715
	set_value: function(_value) {
716
		this.options.value = _value;
717
718
		var display = this._convert_to_display(_value);
719
720
		// Set display
721
		if(this.duration[0].nodeName == "INPUT")
722
		{
723
			this.duration.val(display.value);
724
		}
725
		else
726
		{
727
			this.duration.text(display.value + " ");
728
		}
729
730
		// Set unit as figured for display
731
		if(display.unit != this.options.display_format)
732
		{
733
			if(this.format && this.format.children().length > 1) {
734
				$j("option[value='"+display.unit+"']",this.format).attr('selected','selected');
735
			}
736
			else
737
			{
738
				this.format.text(display.unit ? this.time_formats[display.unit] : '');
739
			}
740
		}
741
	},
742
743
	/**
744
	 * Converts the value in data format into value in display format.
745
	 *
746
	 * @param _value int/float Data in data format
747
	 *
748
	 * @return Object {value: Value in display format, unit: unit for display}
749
	 */
750
	_convert_to_display: function(_value) {
751
		if (_value)
752
		{
753
			// Put value into minutes for further processing
754
			switch(this.options.data_format)
0 ignored issues
show
Coding Style introduced by
As per coding-style, switch statements should have a default case.
Loading history...
755
			{
756
				case 'd':
757
					_value *= this.options.hours_per_day;
758
					// fall-through
759
				case 'h':
760
					_value *= 60;
761
					break;
762
			}
763
		}
764
765
		// Figure out best unit for display
766
		var _unit = this.options.display_format == "d" ? "d" : "h";
767
		if (this.options.display_format.indexOf('m') > -1 && _value && _value < 60)
768
		{
769
			_unit = 'm';
770
		}
771
		else if (this.options.display_format.indexOf('d') > -1 && _value >= 60*this.options.hours_per_day)
772
		{
773
			_unit = 'd';
774
		}
775
		_value = this.options.empty_not_0 && _value === '' || !this.options.empty_not_0 && !_value ? '' :
776
			(_unit == 'm' ? parseInt( _value) : (Math.round((_value / 60.0 / (_unit == 'd' ? this.options.hours_per_day : 1))*100)/100));
777
778
		if(_value === '') _unit = '';
779
780
		// use decimal separator from user prefs
781
		var format = this.egw().preference('number_format');
782
		var sep = format ? format[0] : '.';
783
		if (typeof _value == 'string' && format && sep && sep != '.')
784
		{
785
			_value = _value.replace('.',sep);
786
		}
787
788
		return {value: _value, unit:_unit};
789
	},
790
791
	/**
792
	 * Change displayed value into storage value and return
793
	 */
794
	getValue: function() {
795
		var value = this.duration.val().replace(',', '.');
796
		if(value === '')
797
		{
798
			return this.options.empty_not_0 ? '' : 0;
799
		}
800
		// Put value into minutes for further processing
801
		switch(this.format && this.format.val() ? this.format.val() : this.options.display_format)
0 ignored issues
show
Coding Style introduced by
As per coding-style, switch statements should have a default case.
Loading history...
802
		{
803
			case 'd':
804
				value *= this.options.hours_per_day;
805
				// fall-through
806
			case 'h':
807
				value *= 60;
808
				break;
809
		}
810
		// Minutes should be an integer.  Floating point math.
811
		value = Math.round(value);
812
		
813
		switch(this.options.data_format)
0 ignored issues
show
Coding Style introduced by
As per coding-style, switch statements should have a default case.
Loading history...
814
		{
815
			case 'd':
816
				value /= this.options.hours_per_day;
817
				// fall-through
818
			case 'h':
819
				value /= 60.0;
820
				break;
821
		}
822
		return value;
823
	}
824
});
825
et2_register_widget(et2_date_duration, ["date-duration"]);
826
827
/**
828
 * @augments et2_date_duration
829
 */
830
var et2_date_duration_ro = et2_date_duration.extend([et2_IDetachedDOM],
831
{
832
	/**
833
	 * @memberOf et2_date_duration_ro
834
	 */
835
	createInputWidget: function() {
836
		this.node = $j(document.createElement("span"));
837
		this.duration = $j(document.createElement("span")).appendTo(this.node);
838
		this.format = $j(document.createElement("span")).appendTo(this.node);
839
	},
840
841
	/**
842
	 * Code for implementing et2_IDetachedDOM
843
	 * Fast-clonable read-only widget that only deals with DOM nodes, not the widget tree
844
	 */
845
846
	/**
847
	 * Build a list of attributes which can be set when working in the
848
	 * "detached" mode in the _attrs array which is provided
849
	 * by the calling code.
850
	 *
851
	 * @param {array} _attrs array to add further attributes to
852
	 */
853
	getDetachedAttributes: function(_attrs) {
854
		_attrs.push("value");
855
	},
856
857
	/**
858
	 * Returns an array of DOM nodes. The (relativly) same DOM-Nodes have to be
859
	 * passed to the "setDetachedAttributes" function in the same order.
860
	 *
861
	 * @return {array}
862
	 */
863
	getDetachedNodes: function() {
864
		return [this.duration[0], this.format[0]];
865
	},
866
867
	/**
868
	 * Sets the given associative attribute->value array and applies the
869
	 * attributes to the given DOM-Node.
870
	 *
871
	 * @param _nodes is an array of nodes which has to be in the same order as
872
	 *      the nodes returned by "getDetachedNodes"
873
	 * @param _values is an associative array which contains a subset of attributes
874
	 *      returned by the "getDetachedAttributes" function and sets them to the
875
	 *      given values.
876
	 */
877
	setDetachedAttributes: function(_nodes, _values) {
878
		for(var i = 0; i < _nodes.length; i++) {
879
			// Clear the node
880
			for (var j = _nodes[i].childNodes.length - 1; j >= 0; j--)
881
			{
882
				_nodes[i].removeChild(_nodes[i].childNodes[j]);
883
			}
884
		}
885
		if(typeof _values.value !== 'undefined')
886
		{
887
			_values.value = parseFloat(_values.value);
888
		}
889
		if(_values.value)
890
		{
891
			var display = this._convert_to_display(_values.value);
892
			_nodes[0].appendChild(document.createTextNode(display.value));
893
			_nodes[1].appendChild(document.createTextNode(display.unit));
894
		}
895
	}
896
897
});
898
et2_register_widget(et2_date_duration_ro, ["date-duration_ro"]);
899
900
/**
901
 * et2_date_ro is the readonly implementation of some date widget.
902
 * @augments et2_valueWidget
903
 */
904
var et2_date_ro = et2_valueWidget.extend([et2_IDetachedDOM],
905
{
906
	/**
907
	 * Ignore all more advanced attributes.
908
	 */
909
	attributes: {
910
		"value": {
911
			"type": "string"
912
		},
913
		"type": {
914
			"ignore": false
915
		},
916
		"data_format": {
917
			"ignore": true,
918
			"description": "Format data is in.  This is not used client-side because it's always a timestamp client side."
919
		},
920
		min: {ignore: true},
921
		max: {ignore: true},
922
		year_range: {ignore: true}
923
	},
924
925
	legacyOptions: ["data_format"],
926
927
	/**
928
	 * Internal container for working easily with dates
929
	 */
930
	date: new Date(),
931
932
	/**
933
	 * Constructor
934
	 *
935
	 * @memberOf et2_date_ro
936
	 */
937
	init: function() {
938
		this._super.apply(this, arguments);
939
		this._labelContainer = $j(document.createElement("label"))
940
			.addClass("et2_label");
941
		this.value = "";
942
		this.span = $j(document.createElement(this._type == "date-since" || this._type == "date-time_today" ? "span" : "time"))
943
			.addClass("et2_date_ro et2_label")
944
			.appendTo(this._labelContainer);
945
946
		this.setDOMNode(this._labelContainer[0]);
947
	},
948
949
	set_value: function(_value) {
950
		if(typeof _value == 'undefined') _value = 0;
951
952
		this.value = _value;
953
954
		if(_value == 0 || _value == null)
955
		{
956
			this.span.attr("datetime", "").text("");
957
			return;
958
		}
959
960
		if(typeof _value == 'string' && _value.match(/(\d{4})-(\d{2})-(\d{2})T(\d{2})\:(\d{2})\:(\d{2})(?:\.\d{3})?(?:Z|[+-](\d{2})\:(\d{2}))/))
961
		{
962
			this.date = new Date(_value);
963
			this.date = new Date(this.date.valueOf() + (this.date.getTimezoneOffset()*60*1000));
964
		}
965
		else if(typeof _value == 'string' && isNaN(_value))
966
		{
967
			try {
968
				// parseDateTime to handle string PHP: DateTime local date/time format
969
				var parsed = (typeof jQuery.datepicker.parseDateTime("yy-mm-dd","hh:mm:ss", _value) !='undefined')?
970
							jQuery.datepicker.parseDateTime("yy-mm-dd","hh:mm:ss", _value):
971
							jQuery.datepicker.parseDateTime(this.egw().preference('dateformat'),this.egw().preference('timeformat') == '24' ? 'H:i' : 'g:i a', _value);
972
			}
973
			// display unparsable dates as empty
974
			catch(e) {
975
				this.span.attr("datetime", "").text("");
976
				return;
977
			}
978
			var text = new Date(parsed);
979
980
			// Update local variable, but remove the timezone offset that javascript adds
981
			if(parsed)
982
			{
983
				this.date = new Date(text.valueOf() - (text.getTimezoneOffset()*60*1000));
984
			}
985
986
			// JS dates use milliseconds
987
			this.date.setTime(text.valueOf());
988
		}
989
		else
990
		{
991
			// _value is timestamp in usertime, ready to be used with date() function identical to PHP date()
992
			this.date = _value;
993
		}
994
		var display = this.date.toString();
995
996
		switch(this._type) {
0 ignored issues
show
Coding Style introduced by
As per coding-style, switch statements should have a default case.
Loading history...
997
			case "time_or_date":
998
			case "date-time_today":
999
				// Today - just the time
1000
				if(date('Y-m-d', this.date) == date('Y-m-d'))
1001
				{
1002
					display = date(this.egw().preference('timeformat') == '24' ? 'H:i' : 'g:i a', this.date);
1003
				}
1004
				else if (this._type === "time_or_date")
1005
				{
1006
					display = date(this.egw().preference('dateformat'), this.date);
1007
				}
1008
				// Before today - date and time
1009
				else
1010
				{
1011
					display = date(this.egw().preference('dateformat') + " " +
1012
						(this.egw().preference('timeformat') == '24' ? 'H:i' : 'g:i a'), this.date);
1013
				}
1014
				break;
1015
			case "date":
1016
				display = date(this.egw().preference('dateformat'), this.date);
1017
				break;
1018
			case "date-timeonly":
1019
				display = date(this.egw().preference('timeformat') == '24' ? 'H:i' : 'g:i a', this.date);
1020
				break;
1021
			case "date-time":
1022
				display = date(this.egw().preference('dateformat') + " " +
1023
					(this.egw().preference('timeformat') == '24' ? 'H:i' : 'g:i a'), this.date);
1024
				break;
1025
			case "date-since":
1026
				var unit2label = {
1027
					'Y': 'years',
1028
					'm': 'month',
1029
					'd': 'days',
1030
					'H': 'hours',
1031
					'i': 'minutes',
1032
					's': 'seconds'
1033
				};
1034
				var unit2s = {
1035
					'Y': 31536000,
1036
					'm': 2628000,
1037
					'd': 86400,
1038
					'H': 3600,
1039
					'i': 60,
1040
					's': 1
1041
				};
1042
				var d = new Date();
1043
				var diff = Math.round(d.valueOf() / 1000) - Math.round(this.date.valueOf()/1000);
1044
				display = '';
1045
1046
				for(var unit in unit2s)
0 ignored issues
show
introduced by
This code is unreachable and can thus be removed without consequences.
Loading history...
Bug introduced by
The variable unit2s seems to be never initialized.
Loading history...
1047
				{
1048
					var unit_s = unit2s[unit];
1049
					if (diff >= unit_s || unit == 's')
1050
					{
1051
						display = Math.round(diff/unit_s,1)+' '+this.egw().lang(unit2label[unit]);
1052
						break;
1053
					}
1054
				}
1055
				break;
1056
		}
1057
		this.span.attr("datetime", date("Y-m-d H:i:s",this.date)).text(display);
1058
	},
1059
1060
	set_label: function(label)
1061
	{
1062
		// Remove current label
1063
		this._labelContainer.contents()
1064
			.filter(function(){ return this.nodeType == 3; }).remove();
1065
1066
		var parts = et2_csvSplit(label, 2, "%s");
1067
		this._labelContainer.prepend(parts[0]);
1068
		this._labelContainer.append(parts[1]);
1069
		this.label = label;
1070
	},
1071
1072
	/**
1073
	 * Creates a list of attributes which can be set when working in the
1074
	 * "detached" mode. The result is stored in the _attrs array which is provided
1075
	 * by the calling code.
1076
	 *
1077
	 * @param {array} _attrs array to add further attributes to
1078
	 */
1079
	getDetachedAttributes: function(_attrs) {
1080
		_attrs.push("label", "value","class");
1081
	},
1082
1083
	/**
1084
	 * Returns an array of DOM nodes. The (relatively) same DOM-Nodes have to be
1085
	 * passed to the "setDetachedAttributes" function in the same order.
1086
	 *
1087
	 * @return {array}
1088
	 */
1089
	getDetachedNodes: function() {
1090
		return [this._labelContainer[0], this.span[0]];
1091
	},
1092
1093
	/**
1094
	 * Sets the given associative attribute->value array and applies the
1095
	 * attributes to the given DOM-Node.
1096
	 *
1097
	 * @param _nodes is an array of nodes which have to be in the same order as
1098
	 *      the nodes returned by "getDetachedNodes"
1099
	 * @param _values is an associative array which contains a subset of attributes
1100
	 *      returned by the "getDetachedAttributes" function and sets them to the
1101
	 *      given values.
1102
	 */
1103
	setDetachedAttributes: function(_nodes, _values) {
1104
		this._labelContainer = jQuery(_nodes[0]);
1105
		this.span = jQuery(_nodes[1]);
1106
1107
		this.set_value(_values["value"]);
1108
		if(_values["label"])
1109
		{
1110
			this.set_label(_values["label"]);
1111
		}
1112
		if(_values["class"])
1113
		{
1114
			this.span.addClass(_values["class"]);
1115
		}
1116
	}
1117
});
1118
et2_register_widget(et2_date_ro, ["date_ro", "date-time_ro", "date-since", "date-time_today", "time_or_date", "date-timeonly_ro"]);
1119
1120
1121
/**
1122
 * Widget for selecting a date range
1123
 *
1124
 * @augments et2_inputWidget
1125
 */
1126
var et2_date_range = et2_inputWidget.extend({
1127
	attributes: {
1128
		value: {
1129
			"type": "any",
1130
			"description": "An object with keys 'from' and 'to' for absolute ranges, or a relative range string"
1131
		},
1132
		relative: {
1133
			name: 'Relative',
1134
			type: 'boolean',
1135
			description: 'Is the date range relative (this week) or absolute (2016-02-15 - 2016-02-21).  This will affect the value returned.'
1136
		}
1137
	},
1138
1139
	/**
1140
	 * Constructor
1141
	 *
1142
	 * @memberOf et2_number
1143
	 */
1144
	init: function init() {
1145
		this._super.apply(this, arguments);
1146
1147
		this.div = jQuery(document.createElement('div'))
1148
			.attr({	class:'et2_date_range'});
1149
1150
		this.from = null;
1151
		this.to = null;
1152
		this.select = null;
1153
1154
		// Set domid
1155
		this.set_id(this.id);
1156
1157
		this.setDOMNode(this.div[0]);
1158
		this._createWidget();
1159
1160
		this.set_relative(this.options.relative || false)
1161
	},
1162
1163
	_createWidget: function createInputWidget() {
1164
		var widget = this;
1165
1166
		this.from = et2_createWidget('date',{
1167
			id: this.id+'[from]',
1168
			blur: egw.lang('From'),
1169
			onchange: function() { widget.to.set_min(widget.from.getValue()); }
1170
		},this);
1171
		this.to = et2_createWidget('date',{
1172
			id: this.id+'[to]',
1173
			blur: egw.lang('To'),
1174
			onchange: function() {widget.from.set_max(widget.to.getValue()); }
1175
		},this);
1176
		this.select = et2_createWidget('select',{
1177
			id: this.id+'[relative]',
1178
			select_options: et2_date_range.relative_dates,
1179
			empty_label: this.options.blur || 'All'
1180
		},this);
1181
		this.select.loadingFinished();
1182
	},
1183
1184
	/**
1185
	 * Function which allows iterating over the complete widget tree.
1186
	 * Overridden here to avoid problems with children when getting value
1187
	 *
1188
	 * @param _callback is the function which should be called for each widget
1189
	 * @param _context is the context in which the function should be executed
1190
	 * @param _type is an optional parameter which specifies a class/interface
1191
	 * 	the elements have to be instanceOf.
1192
	 */
1193
	iterateOver: function(_callback, _context, _type) {
1194
		if (typeof _type == "undefined")
1195
		{
1196
			_type = et2_widget;
1197
		}
1198
1199
		if (this.isInTree() && this.instanceOf(_type))
1200
		{
1201
			_callback.call(_context, this);
1202
		}
1203
	},
1204
1205
	/**
1206
	 * Toggles relative or absolute dates
1207
	 * 
1208
	 * @param {boolean} _value
1209
	 */
1210
	set_relative: function set_relative(_value)
1211
	{
1212
		this.options.relative = _value;
1213
		if(this.options.relative)
1214
		{
1215
			$j(this.from.getDOMNode()).hide();
1216
			$j(this.to.getDOMNode()).hide();
1217
		}
1218
		else
1219
		{
1220
			$j(this.select.getDOMNode()).hide();
1221
		}
1222
	},
1223
1224
	set_value: function set_value(value)
1225
	{
1226
		if(!value || typeof value == 'null')
1227
		{
1228
			this.select.set_value('');
1229
			this.from.set_value(null);
1230
			this.to.set_value(null);
1231
		}
1232
1233
		// Relative
1234
		if(value && typeof value === 'string')
1235
		{
1236
			this._set_relative_value(value);
1237
1238
		}
1239
		else if(value && typeof value.from === 'undefined' && value[0])
1240
		{
1241
			value = {
1242
				from: value[0],
1243
				to: value[1] || new Date().valueOf()/1000
1244
			}
1245
		}
1246
		else if (value && value.from && value.to)
1247
		{
1248
			this.from.set_value(value.from);
1249
			this.to.set_value(value.to);
1250
		}
1251
	},
1252
1253
	getValue: function getValue()
1254
	{
1255
		return this.options.relative ?
1256
			this.select.getValue() :
1257
			{ from: this.from.getValue(), to: this.to.getValue() }
1258
	},
1259
1260
	_set_relative_value: function(_value)
1261
	{
1262
		if(this.options.relative)
1263
		{
1264
			$j(this.select.getDOMNode()).show();
1265
		}
1266
		// Show description
1267
		this.select.set_value(_value);
1268
1269
		var now = new Date();
1270
		now.setUTCMinutes(-now.getTimezoneOffset());
1271
		now.setUTCHours(0);
1272
		now.setUTCSeconds(0);
1273
1274
1275
		// Use strings to avoid references
1276
		this.from.set_value(now.toJSON());
1277
		this.to.set_value(now.toJSON());
1278
1279
		var relative = null;
1280
		for(var index in et2_date_range.relative_dates)
1281
		{
1282
			if(et2_date_range.relative_dates[index].value === _value)
1283
			{
1284
				relative = et2_date_range.relative_dates[index];
1285
				break;
1286
			}
1287
		}
1288
		if(relative)
1289
		{
1290
			var dates = ["from","to"]
1291
			var value = now.toJSON();
1292
			for(var i = 0; i < dates.length; i++)
1293
			{
1294
				var date = dates[i];
1295
				if(typeof relative[date] == "function")
1296
				{
1297
					value = relative[date](new Date(value))
1298
				}
1299
				else
1300
				{
1301
					value = this[date]._relativeDate(relative[date]);
1302
				}
1303
				this[date].set_value(value);
1304
			}
1305
		}
1306
	}
1307
});
1308
et2_register_widget(et2_date_range, ["date-range"]);
1309
// Static part of the date range class
1310
jQuery.extend(et2_date_range,
1311
{
1312
	// Class Constants
1313
	relative_dates: [
1314
		// Start and end are relative offsets, see et2_date.set_min()
1315
		// or Date objects
1316
		{
1317
			value: 'Today',
1318
			label: 'Today',
1319
			from: '',
1320
			to: '+1d'
1321
		},
1322
		{
1323
			label: 'Yesterday',
1324
			value: 'Yesterday',
1325
			from: '-1d',
1326
			to: ''
1327
		},
1328
		{
1329
			label: 'This week',
1330
			value: 'This week',
1331
			from: function(date) {return egw.week_start(date);},
1332
			to: function(date) {
1333
				date.setUTCDate(date.getUTCDate() + 6);
1334
				return date;
1335
			}
1336
		},
1337
		{
1338
			label: 'Last week',
1339
			value: 'Last week',
1340
			from: function(date) {
1341
				var d = egw.week_start(date);
1342
				d.setUTCDate(d.getUTCDate() - 7);
1343
				return d;
1344
			},
1345
			to: function(date) {
1346
				date.setUTCDate(date.getUTCDate() + 6);
1347
				return date;
1348
			}
1349
		},
1350
		{
1351
			label: 'This month',
1352
			value: 'This month',
1353
			from: '',
1354
			to: '+1m'
1355
		},
1356
		{
1357
			label: 'Last month',
1358
			value: 'Last month',
1359
			from: '-1m',
1360
			to: ''
1361
		},
1362
		{
1363
			label: 'Last 3 months',
1364
			value: 'Last 3 months',
1365
			from: '-3m',
1366
			to: ''
1367
		},
1368
		/*
1369
		'This quarter'=> array(0,0,0,0,  0,0,0,0),      // Just a marker, needs special handling
1370
		'Last quarter'=> array(0,-4,0,0, 0,-4,0,0),     // Just a marker
1371
		*/
1372
		{
1373
			label: 'This year',
1374
			value: 'This year',
1375
			from: function(d) {
1376
				d.setUTCMonth(0);
1377
				d.setUTCDate(1);
1378
				return d;
1379
			},
1380
			to: function(d) {
1381
				d.setUTCMonth(11);
1382
				d.setUTCDate(31);
1383
				return d
1384
			}
1385
		},
1386
		{
1387
			label: 'Last year',
1388
			value: 'Last year',
1389
			from: function(d) {
1390
				d.setUTCMonth(0);
1391
				d.setUTCDate(1);
1392
				d.setUTCYear(d.getUTCYear() - 1)
1393
				return d;
1394
			},
1395
			to: function(d) {
1396
				d.setUTCMonth(11);
1397
				d.setUTCDate(31);
1398
				d.setUTCYear(d.getUTCYear() - 1)
1399
				return d
1400
			}
1401
		},
1402
		/* Still needed?
1403
		'2 years ago' => array(-2,0,0,0, -1,0,0,0),
1404
		'3 years ago' => array(-3,0,0,0, -2,0,0,0),
1405
		*/
1406
	]
1407
});
1408