Skip to content

Commit

Permalink
feat(input) add support to for week
Browse files Browse the repository at this point in the history
Add support to date filter for outputing week of year

Partially closes angular#757
  • Loading branch information
benlesh committed Dec 17, 2013
1 parent d026c99 commit a5667e4
Show file tree
Hide file tree
Showing 4 changed files with 350 additions and 44 deletions.
221 changes: 200 additions & 21 deletions src/ng/directive/input.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ var URL_REGEXP = /^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\
var EMAIL_REGEXP = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,6}$/;
var NUMBER_REGEXP = /^\s*(\-|\+)?(\d+|(\d*(\.\d*)))\s*$/;
var DATE_REGEXP = /^(\d{4})-(\d{2})-(\d{2})$/;
var DATETIMELOCAL_REGEXP = /^(\d{4})-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d)$/;
var DATETIMELOCAL_REGEXP = /^(\d{4})-(\d\d)-(\d\d)T(\d\d):(\d\d)$/;
var WEEK_REGEXP = /^(\d{4})-W(\d\d)$/;

var inputType = {

Expand Down Expand Up @@ -164,7 +165,7 @@ var inputType = {
* @description
* HTML5 or text input with datetime validation and transformation. In browsers that do not yet support
* the HTML5 date input, a text element will be used. The text must be entered in a valid ISO-8601
* local datetime format (yyyy-MM-ddTHH:mm:ss), for example: `2010-12-28T14:57:12`. Will also accept a valid ISO
* local datetime format (yyyy-MM-ddTHH:mm), for example: `2010-12-28T14:57`. Will also accept a valid ISO
* datetime string or Date object as model input, but will always output a Date object to the model.
*
* @param {string} ngModel Assignable angular expression to data-bind to.
Expand All @@ -183,27 +184,27 @@ var inputType = {
<doc:source>
<script>
function Ctrl($scope) {
$scope.value = '2010-12-28T14:57:12';
$scope.value = new Date(2010, 11, 28, 14, 57); // or '2010-12-28T14:57';
}
</script>
<form name="myForm" ng-controller="Ctrl as dateCtrl">
Pick a date between in 2013:
<input type="date" name="input" ng-model="value"
placeholder="yyyy-MM-dd" min="2013-01-01T00:00:00" max="2013-12-31T00:00:00" required />
<span class="error" ng-show="myForm.input.$error.required">
Required!</span>
<span class="error" ng-show="myForm.input.$error.datetimelocal">
Not a valid date!</span>
<tt>value = {{value}}</tt><br/>
<tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/>
<tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/>
<tt>myForm.$valid = {{myForm.$valid}}</tt><br/>
<tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/>
Pick a date between in 2013:
<input type="datetime-local" name="input" ng-model="value"
placeholder="yyyy-MM-ddTHH:mm" min="2001-01-01T00:00" max="2013-12-31T00:00" required />
<span class="error" ng-show="myForm.input.$error.required">
Required!</span>
<span class="error" ng-show="myForm.input.$error.datetimelocal">
Not a valid date!</span>
<tt>value = {{value}}</tt><br/>
<tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/>
<tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/>
<tt>myForm.$valid = {{myForm.$valid}}</tt><br/>
<tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/>
</form>
</doc:source>
<doc:scenario>
it('should initialize to model', function() {
expect(binding('value')).toEqual('2010-12-28T14:57:12');
expect(binding('value')).toEqual('2010-12-28T14:57');
expect(binding('myForm.input.$valid')).toEqual('true');
});
Expand All @@ -214,14 +215,81 @@ var inputType = {
});
it('should be invalid if over max', function() {
input('value').enter('2015-01-01T23:59:59');
input('value').enter('2015-01-01T23:59');
expect(binding('value')).toEqual('');
expect(binding('myForm.input.$valid')).toEqual('false');
});
</doc:scenario>
</doc:example>
*/
'datetime-local': dateTimeLocalInputType,

/**
* @ngdoc inputType
* @name ng.directive:input.week
*
* @description
* HTML5 or text input with week-of-the-year validation and transformation to Date. In browsers that do not yet support
* the HTML5 week input, a text element will be used. The text must be entered in a valid ISO-8601
* week format (yyyy-W##), for example: `2013-W02`. Will also accept a valid ISO
* week string or Date object as model input, but will always output a Date object to the model.
*
* @param {string} ngModel Assignable angular expression to data-bind to.
* @param {string=} name Property name of the form under which the control is published.
* @param {string=} min Sets the `min` validation error key if the value entered is less than `min`.
* @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`.
* @param {string=} required Sets `required` validation error key if the value is not entered.
* @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to
* the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of
* `required` when you want to data-bind to the `required` attribute.
* @param {string=} ngChange Angular expression to be executed when input changes due to user
* interaction with the input element.
*
* @example
<doc:example>
<doc:source>
<script>
function Ctrl($scope) {
$scope.value = '2013-W01';
}
</script>
<form name="myForm" ng-controller="Ctrl as dateCtrl">
Pick a date between in 2013:
<input type="week" name="input" ng-model="value"
placeholder="YYYY-W##" min="2012-W32" max="2013-W52" required />
<span class="error" ng-show="myForm.input.$error.required">
Required!</span>
<span class="error" ng-show="myForm.input.$error.week">
Not a valid date!</span>
<tt>value = {{value}}</tt><br/>
<tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/>
<tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/>
<tt>myForm.$valid = {{myForm.$valid}}</tt><br/>
<tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/>
</form>
</doc:source>
<doc:scenario>
it('should initialize to model', function() {
expect(binding('value')).toEqual('2013-W01');
expect(binding('myForm.input.$valid')).toEqual('true');
});
it('should be invalid if empty', function() {
input('value').enter('');
expect(binding('value')).toEqual('');
expect(binding('myForm.input.$valid')).toEqual('false');
});
it('should be invalid if over max', function() {
input('value').enter('2015-W01');
expect(binding('value')).toEqual('');
expect(binding('myForm.input.$valid')).toEqual('false');
});
</doc:scenario>
</doc:example>
*/
'week': weekInputType,

/**
* @ngdoc inputType
* @name ng.directive:input.number
Expand Down Expand Up @@ -669,6 +737,118 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) {
}
}


function weekInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter) {
textInputType(scope, element, attr, ctrl, $sniffer, $browser);

ctrl.$parsers.push(function(value) {
if(ctrl.$isEmpty(value)) {
ctrl.$setValidity('week', true);
return value;
}

if(WEEK_REGEXP.test(value)) {
ctrl.$setValidity('week', true);
return new Date(getTime(value).time);
}

ctrl.$setValidity('week', false);
return undefined;
});

ctrl.$formatters.push(function(value) {
if(isDate(value)) {
return $filter('date')(value, 'yyyy-Www');
}
return ctrl.$isEmpty(value) ? '' : ''+value;
});

if(attr.min) {
var minValidator = function(value) {
var valTime = getTime(value),
minTime = getTime(attr.min);

var valid = ctrl.$isEmpty(value) ||
valTime.time >= minTime.time;

ctrl.$setValidity('min', valid);
return valid ? value : undefined;
};

ctrl.$parsers.push(minValidator);
ctrl.$formatters.push(minValidator);
}

if(attr.max) {
var maxValidator = function(value) {
var valTime = getTime(value),
maxTime = getTime(attr.max);

var valid = ctrl.$isEmpty(value) ||
valTime.time <= maxTime.time;

ctrl.$setValidity('max', valid);
return valid ? value : undefined;
};

ctrl.$parsers.push(maxValidator);
ctrl.$formatters.push(maxValidator);
}

function getFirstThursday(year) {
var d = 1, date;
while(true) {
date = new Date(year, 0, d++);
if(date.getDay() === 4) {
return date;
}
}
}

function getThisThursday(date) {
return new Date(date.getFullYear(), date.getMonth(), date.getDate() + (4 - date.getDay()));
}

var MILLISECONDS_PER_WEEK = 6.048e8;

function getWeek(date) {
var firstThurs = getFirstThursday(date.getFullYear()),
thisThurs = getThisThursday(date),
diff = +thisThurs - +firstThurs;

return 1 + Math.round(diff / MILLISECONDS_PER_WEEK);
}

function getTime(isoWeek) {
if(isDate(isoWeek)) {
return {
year: isoWeek.getFullYear(),
week: getWeek(isoWeek),
time: +isoWeek
};
}

if(isString(isoWeek)) {
WEEK_REGEXP.lastIndex = 0;
var parts = WEEK_REGEXP.exec(isoWeek);
if(parts) {
var year = +parts[1],
week = +parts[2],
firstThurs = getFirstThursday(year),
addDays = (week - 1) * 7;

return {
time: +new Date(year, 0, firstThurs.getDate() + addDays),
week: week,
year: year
};
}
}

return NaN;
}
}

function dateTimeLocalInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter) {
textInputType(scope, element, attr, ctrl, $sniffer, $browser);

Expand All @@ -689,7 +869,7 @@ function dateTimeLocalInputType(scope, element, attr, ctrl, $sniffer, $browser,

ctrl.$formatters.push(function(value) {
if(isDate(value)) {
return $filter('date')(value, 'yyyy-MM-ddTHH:mm:ss');
return $filter('date')(value, 'yyyy-MM-ddTHH:mm');
}
return ctrl.$isEmpty(value) ? '' : '' + value;
});
Expand Down Expand Up @@ -730,10 +910,9 @@ function dateTimeLocalInputType(scope, element, attr, ctrl, $sniffer, $browser,
MM = +parts[2] - 1,
dd = +parts[3],
HH = +parts[4],
mm = +parts[5],
ss = +parts[6];
mm = +parts[5];

return +new Date(yyyy, MM, dd, HH, mm, ss);
return +new Date(yyyy, MM, dd, HH, mm);
}

return NaN;
Expand Down
37 changes: 35 additions & 2 deletions src/ng/filter/filters.js
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,35 @@ function timeZoneGetter(date) {
return paddedZone;
}

function getFirstThursday(year) {
var d = 1,
date = new Date(year, 0, d);
while(date.getDay() !== 4) {
d++;
date = new Date(year, 0, d);
}
return date;
}

function getThursdayThisWeek(date) {
var day = date.getDay(),
d = date.getDate();

return new Date(date.getFullYear(), date.getMonth(), d + (4 - day));
}

function weekGetter(size) {
return function(date) {
var firstThurs = getFirstThursday(date.getFullYear()),
thisThurs = getThursdayThisWeek(date);

var diff = +thisThurs - +firstThurs,
result = 1 + Math.round(diff / 6.048e8);

return padNumber(result, size);
};
}

function ampmGetter(date, formats) {
return date.getHours() < 12 ? formats.AMPMS[0] : formats.AMPMS[1];
}
Expand Down Expand Up @@ -256,10 +285,12 @@ var DATE_FORMATS = {
EEEE: dateStrGetter('Day'),
EEE: dateStrGetter('Day', true),
a: ampmGetter,
Z: timeZoneGetter
Z: timeZoneGetter,
ww: weekGetter(2),
w: weekGetter(1)
};

var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZE']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+|H+|h+|m+|s+|a|Z))(.*)/,
var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZEw']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+|H+|h+|m+|s+|a|Z|w+))(.*)/,
NUMBER_STRING = /^\-?\d+$/;

/**
Expand Down Expand Up @@ -294,6 +325,8 @@ var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZE']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+
* * `'.sss' or ',sss'`: Millisecond in second, padded (000-999)
* * `'a'`: am/pm marker
* * `'Z'`: 4 digit (+sign) representation of the timezone offset (-1200-+1200)
* * `'ww'`: ISO-8601 week of year (00-53)
* * `'w'`: ISO-8601 week of year (0-53)
*
* `format` string can also be one of the following predefined
* {@link guide/i18n localizable formats}:
Expand Down
Loading

0 comments on commit a5667e4

Please sign in to comment.