angular-cached-resource
Version:
An AngularJS module to interact with RESTful resources, even when browser is offline
1,328 lines (1,321 loc) • 135 kB
JavaScript
/**
* angular-strap
* @version v2.0.1 - 2014-04-10
* @link http://mgcrea.github.io/angular-strap
* @author Olivier Louvignes (olivier@mg-crea.com)
* @license MIT License, http://www.opensource.org/licenses/MIT
*/
(function(window, document, undefined) {
'use strict';
// Source: module.js
angular.module('mgcrea.ngStrap', [
'mgcrea.ngStrap.modal',
'mgcrea.ngStrap.aside',
'mgcrea.ngStrap.alert',
'mgcrea.ngStrap.button',
'mgcrea.ngStrap.select',
'mgcrea.ngStrap.datepicker',
'mgcrea.ngStrap.timepicker',
'mgcrea.ngStrap.navbar',
'mgcrea.ngStrap.tooltip',
'mgcrea.ngStrap.popover',
'mgcrea.ngStrap.dropdown',
'mgcrea.ngStrap.typeahead',
'mgcrea.ngStrap.scrollspy',
'mgcrea.ngStrap.affix',
'mgcrea.ngStrap.tab'
]);
// Source: affix.js
angular.module('mgcrea.ngStrap.affix', [
'mgcrea.ngStrap.helpers.dimensions',
'mgcrea.ngStrap.helpers.debounce'
]).provider('$affix', function () {
var defaults = this.defaults = { offsetTop: 'auto' };
this.$get = [
'$window',
'debounce',
'dimensions',
function ($window, debounce, dimensions) {
var bodyEl = angular.element($window.document.body);
var windowEl = angular.element($window);
function AffixFactory(element, config) {
var $affix = {};
// Common vars
var options = angular.extend({}, defaults, config);
var targetEl = options.target;
// Initial private vars
var reset = 'affix affix-top affix-bottom', initialAffixTop = 0, initialOffsetTop = 0, offsetTop = 0, offsetBottom = 0, affixed = null, unpin = null;
var parent = element.parent();
// Options: custom parent
if (options.offsetParent) {
if (options.offsetParent.match(/^\d+$/)) {
for (var i = 0; i < options.offsetParent * 1 - 1; i++) {
parent = parent.parent();
}
} else {
parent = angular.element(options.offsetParent);
}
}
$affix.init = function () {
$affix.$parseOffsets();
initialOffsetTop = dimensions.offset(element[0]).top + initialAffixTop;
// Bind events
targetEl.on('scroll', $affix.checkPosition);
targetEl.on('click', $affix.checkPositionWithEventLoop);
windowEl.on('resize', $affix.$debouncedOnResize);
// Both of these checkPosition() calls are necessary for the case where
// the user hits refresh after scrolling to the bottom of the page.
$affix.checkPosition();
$affix.checkPositionWithEventLoop();
};
$affix.destroy = function () {
// Unbind events
targetEl.off('scroll', $affix.checkPosition);
targetEl.off('click', $affix.checkPositionWithEventLoop);
windowEl.off('resize', $affix.$debouncedOnResize);
};
$affix.checkPositionWithEventLoop = function () {
setTimeout($affix.checkPosition, 1);
};
$affix.checkPosition = function () {
// if (!this.$element.is(':visible')) return
var scrollTop = getScrollTop();
var position = dimensions.offset(element[0]);
var elementHeight = dimensions.height(element[0]);
// Get required affix class according to position
var affix = getRequiredAffixClass(unpin, position, elementHeight);
// Did affix status changed this last check?
if (affixed === affix)
return;
affixed = affix;
// Add proper affix class
element.removeClass(reset).addClass('affix' + (affix !== 'middle' ? '-' + affix : ''));
if (affix === 'top') {
unpin = null;
element.css('position', options.offsetParent ? '' : 'relative');
element.css('top', '');
} else if (affix === 'bottom') {
if (options.offsetUnpin) {
unpin = -(options.offsetUnpin * 1);
} else {
// Calculate unpin threshold when affixed to bottom.
// Hopefully the browser scrolls pixel by pixel.
unpin = position.top - scrollTop;
}
element.css('position', options.offsetParent ? '' : 'relative');
element.css('top', options.offsetParent ? '' : bodyEl[0].offsetHeight - offsetBottom - elementHeight - initialOffsetTop + 'px');
} else {
// affix === 'middle'
unpin = null;
element.css('position', 'fixed');
element.css('top', initialAffixTop + 'px');
}
};
$affix.$onResize = function () {
$affix.$parseOffsets();
$affix.checkPosition();
};
$affix.$debouncedOnResize = debounce($affix.$onResize, 50);
$affix.$parseOffsets = function () {
// Reset position to calculate correct offsetTop
element.css('position', options.offsetParent ? '' : 'relative');
if (options.offsetTop) {
if (options.offsetTop === 'auto') {
options.offsetTop = '+0';
}
if (options.offsetTop.match(/^[-+]\d+$/)) {
initialAffixTop = -options.offsetTop * 1;
if (options.offsetParent) {
offsetTop = dimensions.offset(parent[0]).top + options.offsetTop * 1;
} else {
offsetTop = dimensions.offset(element[0]).top - dimensions.css(element[0], 'marginTop', true) + options.offsetTop * 1;
}
} else {
offsetTop = options.offsetTop * 1;
}
}
if (options.offsetBottom) {
if (options.offsetParent && options.offsetBottom.match(/^[-+]\d+$/)) {
// add 1 pixel due to rounding problems...
offsetBottom = getScrollHeight() - (dimensions.offset(parent[0]).top + dimensions.height(parent[0])) + options.offsetBottom * 1 + 1;
} else {
offsetBottom = options.offsetBottom * 1;
}
}
};
// Private methods
function getRequiredAffixClass(unpin, position, elementHeight) {
var scrollTop = getScrollTop();
var scrollHeight = getScrollHeight();
if (scrollTop <= offsetTop) {
return 'top';
} else if (unpin !== null && scrollTop + unpin <= position.top) {
return 'middle';
} else if (offsetBottom !== null && position.top + elementHeight + initialAffixTop >= scrollHeight - offsetBottom) {
return 'bottom';
} else {
return 'middle';
}
}
function getScrollTop() {
return targetEl[0] === $window ? $window.pageYOffset : targetEl[0] === $window;
}
function getScrollHeight() {
return targetEl[0] === $window ? $window.document.body.scrollHeight : targetEl[0].scrollHeight;
}
$affix.init();
return $affix;
}
return AffixFactory;
}
];
}).directive('bsAffix', [
'$affix',
'$window',
function ($affix, $window) {
return {
restrict: 'EAC',
require: '^?bsAffixTarget',
link: function postLink(scope, element, attr, affixTarget) {
var options = {
scope: scope,
offsetTop: 'auto',
target: affixTarget ? affixTarget.$element : angular.element($window)
};
angular.forEach([
'offsetTop',
'offsetBottom',
'offsetParent',
'offsetUnpin'
], function (key) {
if (angular.isDefined(attr[key]))
options[key] = attr[key];
});
var affix = $affix(element, options);
scope.$on('$destroy', function () {
options = null;
affix = null;
});
}
};
}
]).directive('bsAffixTarget', function () {
return {
controller: [
'$element',
function ($element) {
this.$element = $element;
}
]
};
});
// Source: alert.js
// @BUG: following snippet won't compile correctly
// @TODO: submit issue to core
// '<span ng-if="title"><strong ng-bind="title"></strong> </span><span ng-bind-html="content"></span>' +
angular.module('mgcrea.ngStrap.alert', ['mgcrea.ngStrap.modal']).provider('$alert', function () {
var defaults = this.defaults = {
animation: 'am-fade',
prefixClass: 'alert',
placement: null,
template: 'alert/alert.tpl.html',
container: false,
element: null,
backdrop: false,
keyboard: true,
show: true,
duration: false,
type: false
};
this.$get = [
'$modal',
'$timeout',
function ($modal, $timeout) {
function AlertFactory(config) {
var $alert = {};
// Common vars
var options = angular.extend({}, defaults, config);
$alert = $modal(options);
// Support scope as string options [/*title, content, */type]
if (options.type) {
$alert.$scope.type = options.type;
}
// Support auto-close duration
var show = $alert.show;
if (options.duration) {
$alert.show = function () {
show();
$timeout(function () {
$alert.hide();
}, options.duration * 1000);
};
}
return $alert;
}
return AlertFactory;
}
];
}).directive('bsAlert', [
'$window',
'$location',
'$sce',
'$alert',
function ($window, $location, $sce, $alert) {
var requestAnimationFrame = $window.requestAnimationFrame || $window.setTimeout;
return {
restrict: 'EAC',
scope: true,
link: function postLink(scope, element, attr, transclusion) {
// Directive options
var options = {
scope: scope,
element: element,
show: false
};
angular.forEach([
'template',
'placement',
'keyboard',
'html',
'container',
'animation',
'duration'
], function (key) {
if (angular.isDefined(attr[key]))
options[key] = attr[key];
});
// Support scope as data-attrs
angular.forEach([
'title',
'content',
'type'
], function (key) {
attr[key] && attr.$observe(key, function (newValue, oldValue) {
scope[key] = $sce.trustAsHtml(newValue);
});
});
// Support scope as an object
attr.bsAlert && scope.$watch(attr.bsAlert, function (newValue, oldValue) {
if (angular.isObject(newValue)) {
angular.extend(scope, newValue);
} else {
scope.content = newValue;
}
}, true);
// Initialize alert
var alert = $alert(options);
// Trigger
element.on(attr.trigger || 'click', alert.toggle);
// Garbage collection
scope.$on('$destroy', function () {
alert.destroy();
options = null;
alert = null;
});
}
};
}
]);
// Source: aside.js
angular.module('mgcrea.ngStrap.aside', ['mgcrea.ngStrap.modal']).provider('$aside', function () {
var defaults = this.defaults = {
animation: 'am-fade-and-slide-right',
prefixClass: 'aside',
placement: 'right',
template: 'aside/aside.tpl.html',
contentTemplate: false,
container: false,
element: null,
backdrop: true,
keyboard: true,
html: false,
show: true
};
this.$get = [
'$modal',
function ($modal) {
function AsideFactory(config) {
var $aside = {};
// Common vars
var options = angular.extend({}, defaults, config);
$aside = $modal(options);
return $aside;
}
return AsideFactory;
}
];
}).directive('bsAside', [
'$window',
'$location',
'$sce',
'$aside',
function ($window, $location, $sce, $aside) {
var requestAnimationFrame = $window.requestAnimationFrame || $window.setTimeout;
return {
restrict: 'EAC',
scope: true,
link: function postLink(scope, element, attr, transclusion) {
// Directive options
var options = {
scope: scope,
element: element,
show: false
};
angular.forEach([
'template',
'contentTemplate',
'placement',
'backdrop',
'keyboard',
'html',
'container',
'animation'
], function (key) {
if (angular.isDefined(attr[key]))
options[key] = attr[key];
});
// Support scope as data-attrs
angular.forEach([
'title',
'content'
], function (key) {
attr[key] && attr.$observe(key, function (newValue, oldValue) {
scope[key] = $sce.trustAsHtml(newValue);
});
});
// Support scope as an object
attr.bsAside && scope.$watch(attr.bsAside, function (newValue, oldValue) {
if (angular.isObject(newValue)) {
angular.extend(scope, newValue);
} else {
scope.content = newValue;
}
}, true);
// Initialize aside
var aside = $aside(options);
// Trigger
element.on(attr.trigger || 'click', aside.toggle);
// Garbage collection
scope.$on('$destroy', function () {
aside.destroy();
options = null;
aside = null;
});
}
};
}
]);
// Source: button.js
angular.module('mgcrea.ngStrap.button', []).provider('$button', function () {
var defaults = this.defaults = {
activeClass: 'active',
toggleEvent: 'click'
};
this.$get = function () {
return { defaults: defaults };
};
}).directive('bsCheckboxGroup', function () {
return {
restrict: 'A',
require: 'ngModel',
compile: function postLink(element, attr) {
element.attr('data-toggle', 'buttons');
element.removeAttr('ng-model');
var children = element[0].querySelectorAll('input[type="checkbox"]');
angular.forEach(children, function (child) {
var childEl = angular.element(child);
childEl.attr('bs-checkbox', '');
childEl.attr('ng-model', attr.ngModel + '.' + childEl.attr('value'));
});
}
};
}).directive('bsCheckbox', [
'$button',
'$$rAF',
function ($button, $$rAF) {
var defaults = $button.defaults;
var constantValueRegExp = /^(true|false|\d+)$/;
return {
restrict: 'A',
require: 'ngModel',
link: function postLink(scope, element, attr, controller) {
var options = defaults;
// Support label > input[type="checkbox"]
var isInput = element[0].nodeName === 'INPUT';
var activeElement = isInput ? element.parent() : element;
var trueValue = angular.isDefined(attr.trueValue) ? attr.trueValue : true;
if (constantValueRegExp.test(attr.trueValue)) {
trueValue = scope.$eval(attr.trueValue);
}
var falseValue = angular.isDefined(attr.falseValue) ? attr.falseValue : false;
if (constantValueRegExp.test(attr.falseValue)) {
falseValue = scope.$eval(attr.falseValue);
}
// Parse exotic values
var hasExoticValues = typeof trueValue !== 'boolean' || typeof falseValue !== 'boolean';
if (hasExoticValues) {
controller.$parsers.push(function (viewValue) {
// console.warn('$parser', element.attr('ng-model'), 'viewValue', viewValue);
return viewValue ? trueValue : falseValue;
});
// Fix rendering for exotic values
scope.$watch(attr.ngModel, function (newValue, oldValue) {
controller.$render();
});
}
// model -> view
controller.$render = function () {
// console.warn('$render', element.attr('ng-model'), 'controller.$modelValue', typeof controller.$modelValue, controller.$modelValue, 'controller.$viewValue', typeof controller.$viewValue, controller.$viewValue);
var isActive = angular.equals(controller.$modelValue, trueValue);
$$rAF(function () {
if (isInput)
element[0].checked = isActive;
activeElement.toggleClass(options.activeClass, isActive);
});
};
// view -> model
element.bind(options.toggleEvent, function () {
scope.$apply(function () {
// console.warn('!click', element.attr('ng-model'), 'controller.$viewValue', typeof controller.$viewValue, controller.$viewValue, 'controller.$modelValue', typeof controller.$modelValue, controller.$modelValue);
if (!isInput) {
controller.$setViewValue(!activeElement.hasClass('active'));
}
if (!hasExoticValues) {
controller.$render();
}
});
});
}
};
}
]).directive('bsRadioGroup', function () {
return {
restrict: 'A',
require: 'ngModel',
compile: function postLink(element, attr) {
element.attr('data-toggle', 'buttons');
element.removeAttr('ng-model');
var children = element[0].querySelectorAll('input[type="radio"]');
angular.forEach(children, function (child) {
angular.element(child).attr('bs-radio', '');
angular.element(child).attr('ng-model', attr.ngModel);
});
}
};
}).directive('bsRadio', [
'$button',
'$$rAF',
function ($button, $$rAF) {
var defaults = $button.defaults;
var constantValueRegExp = /^(true|false|\d+)$/;
return {
restrict: 'A',
require: 'ngModel',
link: function postLink(scope, element, attr, controller) {
var options = defaults;
// Support `label > input[type="radio"]` markup
var isInput = element[0].nodeName === 'INPUT';
var activeElement = isInput ? element.parent() : element;
var value = constantValueRegExp.test(attr.value) ? scope.$eval(attr.value) : attr.value;
// model -> view
controller.$render = function () {
// console.warn('$render', element.attr('value'), 'controller.$modelValue', typeof controller.$modelValue, controller.$modelValue, 'controller.$viewValue', typeof controller.$viewValue, controller.$viewValue);
var isActive = angular.equals(controller.$modelValue, value);
$$rAF(function () {
if (isInput)
element[0].checked = isActive;
activeElement.toggleClass(options.activeClass, isActive);
});
};
// view -> model
element.bind(options.toggleEvent, function () {
scope.$apply(function () {
// console.warn('!click', element.attr('value'), 'controller.$viewValue', typeof controller.$viewValue, controller.$viewValue, 'controller.$modelValue', typeof controller.$modelValue, controller.$modelValue);
controller.$setViewValue(value);
controller.$render();
});
});
}
};
}
]);
// Source: datepicker.js
angular.module('mgcrea.ngStrap.datepicker', [
'mgcrea.ngStrap.helpers.dateParser',
'mgcrea.ngStrap.tooltip'
]).provider('$datepicker', function () {
var defaults = this.defaults = {
animation: 'am-fade',
prefixClass: 'datepicker',
placement: 'bottom-left',
template: 'datepicker/datepicker.tpl.html',
trigger: 'focus',
container: false,
keyboard: true,
html: false,
delay: 0,
useNative: false,
dateType: 'date',
dateFormat: 'shortDate',
strictFormat: false,
autoclose: false,
minDate: -Infinity,
maxDate: +Infinity,
startView: 0,
minView: 0,
startWeek: 0
};
this.$get = [
'$window',
'$document',
'$rootScope',
'$sce',
'$locale',
'dateFilter',
'datepickerViews',
'$tooltip',
function ($window, $document, $rootScope, $sce, $locale, dateFilter, datepickerViews, $tooltip) {
var bodyEl = angular.element($window.document.body);
var isTouch = 'createTouch' in $window.document;
var isNative = /(ip(a|o)d|iphone|android)/gi.test($window.navigator.userAgent);
if (!defaults.lang)
defaults.lang = $locale.id;
function DatepickerFactory(element, controller, config) {
var $datepicker = $tooltip(element, angular.extend({}, defaults, config));
var parentScope = config.scope;
var options = $datepicker.$options;
var scope = $datepicker.$scope;
if (options.startView)
options.startView -= options.minView;
// View vars
var pickerViews = datepickerViews($datepicker);
$datepicker.$views = pickerViews.views;
var viewDate = pickerViews.viewDate;
scope.$mode = options.startView;
var $picker = $datepicker.$views[scope.$mode];
// Scope methods
scope.$select = function (date) {
$datepicker.select(date);
};
scope.$selectPane = function (value) {
$datepicker.$selectPane(value);
};
scope.$toggleMode = function () {
$datepicker.setMode((scope.$mode + 1) % $datepicker.$views.length);
};
// Public methods
$datepicker.update = function (date) {
// console.warn('$datepicker.update() newValue=%o', date);
if (angular.isDate(date) && !isNaN(date.getTime())) {
$datepicker.$date = date;
$picker.update.call($picker, date);
}
// Build only if pristine
$datepicker.$build(true);
};
$datepicker.select = function (date, keep) {
// console.warn('$datepicker.select', date, scope.$mode);
if (!angular.isDate(controller.$dateValue))
controller.$dateValue = new Date(date);
controller.$dateValue.setFullYear(date.getFullYear(), date.getMonth(), date.getDate());
if (!scope.$mode || keep) {
controller.$setViewValue(controller.$dateValue);
controller.$render();
if (options.autoclose && !keep) {
$datepicker.hide(true);
}
} else {
angular.extend(viewDate, {
year: date.getFullYear(),
month: date.getMonth(),
date: date.getDate()
});
$datepicker.setMode(scope.$mode - 1);
$datepicker.$build();
}
};
$datepicker.setMode = function (mode) {
// console.warn('$datepicker.setMode', mode);
scope.$mode = mode;
$picker = $datepicker.$views[scope.$mode];
$datepicker.$build();
};
// Protected methods
$datepicker.$build = function (pristine) {
// console.warn('$datepicker.$build() viewDate=%o', viewDate);
if (pristine === true && $picker.built)
return;
if (pristine === false && !$picker.built)
return;
$picker.build.call($picker);
};
$datepicker.$updateSelected = function () {
for (var i = 0, l = scope.rows.length; i < l; i++) {
angular.forEach(scope.rows[i], updateSelected);
}
};
$datepicker.$isSelected = function (date) {
return $picker.isSelected(date);
};
$datepicker.$selectPane = function (value) {
var steps = $picker.steps;
var targetDate = new Date(Date.UTC(viewDate.year + (steps.year || 0) * value, viewDate.month + (steps.month || 0) * value, viewDate.date + (steps.day || 0) * value));
angular.extend(viewDate, {
year: targetDate.getUTCFullYear(),
month: targetDate.getUTCMonth(),
date: targetDate.getUTCDate()
});
$datepicker.$build();
};
$datepicker.$onMouseDown = function (evt) {
// Prevent blur on mousedown on .dropdown-menu
evt.preventDefault();
evt.stopPropagation();
// Emulate click for mobile devices
if (isTouch) {
var targetEl = angular.element(evt.target);
if (targetEl[0].nodeName.toLowerCase() !== 'button') {
targetEl = targetEl.parent();
}
targetEl.triggerHandler('click');
}
};
$datepicker.$onKeyDown = function (evt) {
if (!/(38|37|39|40|13)/.test(evt.keyCode) || evt.shiftKey || evt.altKey)
return;
evt.preventDefault();
evt.stopPropagation();
if (evt.keyCode === 13) {
if (!scope.$mode) {
return $datepicker.hide(true);
} else {
return scope.$apply(function () {
$datepicker.setMode(scope.$mode - 1);
});
}
}
// Navigate with keyboard
$picker.onKeyDown(evt);
parentScope.$digest();
};
// Private
function updateSelected(el) {
el.selected = $datepicker.$isSelected(el.date);
}
function focusElement() {
element[0].focus();
}
// Overrides
var _init = $datepicker.init;
$datepicker.init = function () {
if (isNative && options.useNative) {
element.prop('type', 'date');
element.css('-webkit-appearance', 'textfield');
return;
} else if (isTouch) {
element.prop('type', 'text');
element.attr('readonly', 'true');
element.on('click', focusElement);
}
_init();
};
var _destroy = $datepicker.destroy;
$datepicker.destroy = function () {
if (isNative && options.useNative) {
element.off('click', focusElement);
}
_destroy();
};
var _show = $datepicker.show;
$datepicker.show = function () {
_show();
setTimeout(function () {
$datepicker.$element.on(isTouch ? 'touchstart' : 'mousedown', $datepicker.$onMouseDown);
if (options.keyboard) {
element.on('keydown', $datepicker.$onKeyDown);
}
});
};
var _hide = $datepicker.hide;
$datepicker.hide = function (blur) {
$datepicker.$element.off(isTouch ? 'touchstart' : 'mousedown', $datepicker.$onMouseDown);
if (options.keyboard) {
element.off('keydown', $datepicker.$onKeyDown);
}
_hide(blur);
};
return $datepicker;
}
DatepickerFactory.defaults = defaults;
return DatepickerFactory;
}
];
}).directive('bsDatepicker', [
'$window',
'$parse',
'$q',
'$locale',
'dateFilter',
'$datepicker',
'$dateParser',
'$timeout',
function ($window, $parse, $q, $locale, dateFilter, $datepicker, $dateParser, $timeout) {
var defaults = $datepicker.defaults;
var isNative = /(ip(a|o)d|iphone|android)/gi.test($window.navigator.userAgent);
var isNumeric = function (n) {
return !isNaN(parseFloat(n)) && isFinite(n);
};
return {
restrict: 'EAC',
require: 'ngModel',
link: function postLink(scope, element, attr, controller) {
// Directive options
var options = {
scope: scope,
controller: controller
};
angular.forEach([
'placement',
'container',
'delay',
'trigger',
'keyboard',
'html',
'animation',
'template',
'autoclose',
'dateType',
'dateFormat',
'strictFormat',
'startWeek',
'useNative',
'lang',
'startView',
'minView'
], function (key) {
if (angular.isDefined(attr[key]))
options[key] = attr[key];
});
// Initialize datepicker
if (isNative && options.useNative)
options.dateFormat = 'yyyy-MM-dd';
var datepicker = $datepicker(element, controller, options);
options = datepicker.$options;
// Observe attributes for changes
angular.forEach([
'minDate',
'maxDate'
], function (key) {
// console.warn('attr.$observe(%s)', key, attr[key]);
angular.isDefined(attr[key]) && attr.$observe(key, function (newValue) {
// console.warn('attr.$observe(%s)=%o', key, newValue);
if (newValue === 'today') {
var today = new Date();
datepicker.$options[key] = +new Date(today.getFullYear(), today.getMonth(), today.getDate() + (key === 'maxDate' ? 1 : 0), 0, 0, 0, key === 'minDate' ? 0 : -1);
} else if (angular.isString(newValue) && newValue.match(/^".+"$/)) {
// Support {{ dateObj }}
datepicker.$options[key] = +new Date(newValue.substr(1, newValue.length - 2));
} else if (isNumeric(newValue)) {
datepicker.$options[key] = +new Date(parseInt(newValue, 10));
} else {
datepicker.$options[key] = +new Date(newValue);
}
// Build only if dirty
!isNaN(datepicker.$options[key]) && datepicker.$build(false);
});
});
// Watch model for changes
scope.$watch(attr.ngModel, function (newValue, oldValue) {
datepicker.update(controller.$dateValue);
}, true);
var dateParser = $dateParser({
format: options.dateFormat,
lang: options.lang,
strict: options.strictFormat
});
// viewValue -> $parsers -> modelValue
controller.$parsers.unshift(function (viewValue) {
// console.warn('$parser("%s"): viewValue=%o', element.attr('ng-model'), viewValue);
// Null values should correctly reset the model value & validity
if (!viewValue) {
controller.$setValidity('date', true);
return;
}
var parsedDate = dateParser.parse(viewValue, controller.$dateValue);
if (!parsedDate || isNaN(parsedDate.getTime())) {
controller.$setValidity('date', false);
return;
} else {
var isValid = (isNaN(datepicker.$options.minDate) || parsedDate.getTime() >= datepicker.$options.minDate) && (isNaN(datepicker.$options.maxDate) || parsedDate.getTime() <= datepicker.$options.maxDate);
controller.$setValidity('date', isValid);
// Only update the model when we have a valid date
if (isValid)
controller.$dateValue = parsedDate;
}
if (options.dateType === 'string') {
return dateFilter(viewValue, options.dateFormat);
} else if (options.dateType === 'number') {
return controller.$dateValue.getTime();
} else if (options.dateType === 'iso') {
return controller.$dateValue.toISOString();
} else {
return new Date(controller.$dateValue);
}
});
// modelValue -> $formatters -> viewValue
controller.$formatters.push(function (modelValue) {
// console.warn('$formatter("%s"): modelValue=%o (%o)', element.attr('ng-model'), modelValue, typeof modelValue);
var date;
if (angular.isUndefined(modelValue) || modelValue === null) {
date = NaN;
} else if (angular.isDate(modelValue)) {
date = modelValue;
} else if (options.dateType === 'string') {
date = dateParser.parse(modelValue);
} else {
date = new Date(modelValue);
}
// Setup default value?
// if(isNaN(date.getTime())) {
// var today = new Date();
// date = new Date(today.getFullYear(), today.getMonth(), today.getDate(), 0, 0, 0, 0);
// }
controller.$dateValue = date;
return controller.$dateValue;
});
// viewValue -> element
controller.$render = function () {
// console.warn('$render("%s"): viewValue=%o', element.attr('ng-model'), controller.$viewValue);
element.val(!controller.$dateValue || isNaN(controller.$dateValue.getTime()) ? '' : dateFilter(controller.$dateValue, options.dateFormat));
};
// Garbage collection
scope.$on('$destroy', function () {
datepicker.destroy();
options = null;
datepicker = null;
});
}
};
}
]).provider('datepickerViews', function () {
var defaults = this.defaults = {
dayFormat: 'dd',
daySplit: 7
};
// Split array into smaller arrays
function split(arr, size) {
var arrays = [];
while (arr.length > 0) {
arrays.push(arr.splice(0, size));
}
return arrays;
}
// Modulus operator
function mod(n, m) {
return (n % m + m) % m;
}
this.$get = [
'$locale',
'$sce',
'dateFilter',
function ($locale, $sce, dateFilter) {
return function (picker) {
var scope = picker.$scope;
var options = picker.$options;
var weekDaysMin = $locale.DATETIME_FORMATS.SHORTDAY;
var weekDaysLabels = weekDaysMin.slice(options.startWeek).concat(weekDaysMin.slice(0, options.startWeek));
var weekDaysLabelsHtml = $sce.trustAsHtml('<th class="dow text-center">' + weekDaysLabels.join('</th><th class="dow text-center">') + '</th>');
var startDate = picker.$date || new Date();
var viewDate = {
year: startDate.getFullYear(),
month: startDate.getMonth(),
date: startDate.getDate()
};
var timezoneOffset = startDate.getTimezoneOffset() * 60000;
var views = [
{
format: 'dd',
split: 7,
steps: { month: 1 },
update: function (date, force) {
if (!this.built || force || date.getFullYear() !== viewDate.year || date.getMonth() !== viewDate.month) {
angular.extend(viewDate, {
year: picker.$date.getFullYear(),
month: picker.$date.getMonth(),
date: picker.$date.getDate()
});
picker.$build();
} else if (date.getDate() !== viewDate.date) {
viewDate.date = picker.$date.getDate();
picker.$updateSelected();
}
},
build: function () {
var firstDayOfMonth = new Date(viewDate.year, viewDate.month, 1), firstDayOfMonthOffset = firstDayOfMonth.getTimezoneOffset();
var firstDate = new Date(+firstDayOfMonth - mod(firstDayOfMonth.getDay() - options.startWeek, 6) * 86400000), firstDateOffset = firstDate.getTimezoneOffset();
// Handle daylight time switch
if (firstDateOffset !== firstDayOfMonthOffset)
firstDate = new Date(+firstDate + (firstDateOffset - firstDayOfMonthOffset) * 60000);
var days = [], day;
for (var i = 0; i < 42; i++) {
// < 7 * 6
day = new Date(firstDate.getFullYear(), firstDate.getMonth(), firstDate.getDate() + i);
days.push({
date: day,
label: dateFilter(day, this.format),
selected: picker.$date && this.isSelected(day),
muted: day.getMonth() !== viewDate.month,
disabled: this.isDisabled(day)
});
}
scope.title = dateFilter(firstDayOfMonth, 'MMMM yyyy');
scope.labels = weekDaysLabelsHtml;
scope.rows = split(days, this.split);
this.built = true;
},
isSelected: function (date) {
return picker.$date && date.getFullYear() === picker.$date.getFullYear() && date.getMonth() === picker.$date.getMonth() && date.getDate() === picker.$date.getDate();
},
isDisabled: function (date) {
return date.getTime() < options.minDate || date.getTime() > options.maxDate;
},
onKeyDown: function (evt) {
var actualTime = picker.$date.getTime();
if (evt.keyCode === 37)
picker.select(new Date(actualTime - 1 * 86400000), true);
else if (evt.keyCode === 38)
picker.select(new Date(actualTime - 7 * 86400000), true);
else if (evt.keyCode === 39)
picker.select(new Date(actualTime + 1 * 86400000), true);
else if (evt.keyCode === 40)
picker.select(new Date(actualTime + 7 * 86400000), true);
}
},
{
name: 'month',
format: 'MMM',
split: 4,
steps: { year: 1 },
update: function (date, force) {
if (!this.built || date.getFullYear() !== viewDate.year) {
angular.extend(viewDate, {
year: picker.$date.getFullYear(),
month: picker.$date.getMonth(),
date: picker.$date.getDate()
});
picker.$build();
} else if (date.getMonth() !== viewDate.month) {
angular.extend(viewDate, {
month: picker.$date.getMonth(),
date: picker.$date.getDate()
});
picker.$updateSelected();
}
},
build: function () {
var firstMonth = new Date(viewDate.year, 0, 1);
var months = [], month;
for (var i = 0; i < 12; i++) {
month = new Date(viewDate.year, i, 1);
months.push({
date: month,
label: dateFilter(month, this.format),
selected: picker.$isSelected(month),
disabled: this.isDisabled(month)
});
}
scope.title = dateFilter(month, 'yyyy');
scope.labels = false;
scope.rows = split(months, this.split);
this.built = true;
},
isSelected: function (date) {
return picker.$date && date.getFullYear() === picker.$date.getFullYear() && date.getMonth() === picker.$date.getMonth();
},
isDisabled: function (date) {
var lastDate = +new Date(date.getFullYear(), date.getMonth() + 1, 0);
return lastDate < options.minDate || date.getTime() > options.maxDate;
},
onKeyDown: function (evt) {
var actualMonth = picker.$date.getMonth();
if (evt.keyCode === 37)
picker.select(new Date(picker.$date.setMonth(actualMonth - 1)), true);
else if (evt.keyCode === 38)
picker.select(new Date(picker.$date.setMonth(actualMonth - 4)), true);
else if (evt.keyCode === 39)
picker.select(new Date(picker.$date.setMonth(actualMonth + 1)), true);
else if (evt.keyCode === 40)
picker.select(new Date(picker.$date.setMonth(actualMonth + 4)), true);
}
},
{
name: 'year',
format: 'yyyy',
split: 4,
steps: { year: 12 },
update: function (date, force) {
if (!this.built || force || parseInt(date.getFullYear() / 20, 10) !== parseInt(viewDate.year / 20, 10)) {
angular.extend(viewDate, {
year: picker.$date.getFullYear(),
month: picker.$date.getMonth(),
date: picker.$date.getDate()
});
picker.$build();
} else if (date.getFullYear() !== viewDate.year) {
angular.extend(viewDate, {
year: picker.$date.getFullYear(),
month: picker.$date.getMonth(),
date: picker.$date.getDate()
});
picker.$updateSelected();
}
},
build: function () {
var firstYear = viewDate.year - viewDate.year % (this.split * 3);
var years = [], year;
for (var i = 0; i < 12; i++) {
year = new Date(firstYear + i, 0, 1);
years.push({
date: year,
label: dateFilter(year, this.format),
selected: picker.$isSelected(year),
disabled: this.isDisabled(year)
});
}
scope.title = years[0].label + '-' + years[years.length - 1].label;
scope.labels = false;
scope.rows = split(years, this.split);
this.built = true;
},
isSelected: function (date) {
return picker.$date && date.getFullYear() === picker.$date.getFullYear();
},
isDisabled: function (date) {
var lastDate = +new Date(date.getFullYear() + 1, 0, 0);
return lastDate < options.minDate || date.getTime() > options.maxDate;
},
onKeyDown: function (evt) {
var actualYear = picker.$date.getFullYear();
if (evt.keyCode === 37)
picker.select(new Date(picker.$date.setYear(actualYear - 1)), true);
else if (evt.keyCode === 38)
picker.select(new Date(picker.$date.setYear(actualYear - 4)), true);
else if (evt.keyCode === 39)
picker.select(new Date(picker.$date.setYear(actualYear + 1)), true);
else if (evt.keyCode === 40)
picker.select(new Date(picker.$date.setYear(actualYear + 4)), true);
}
}
];
return {
views: options.minView ? Array.prototype.slice.call(views, options.minView) : views,
viewDate: viewDate
};
};
}
];
});
// Source: dropdown.js
angular.module('mgcrea.ngStrap.dropdown', ['mgcrea.ngStrap.tooltip']).provider('$dropdown', function () {
var defaults = this.defaults = {
animation: 'am-fade',
prefixClass: 'dropdown',
placement: 'bottom-left',
template: 'dropdown/dropdown.tpl.html',
trigger: 'click',
container: false,
keyboard: true,
html: false,
delay: 0
};
this.$get = [
'$window',
'$rootScope',
'$tooltip',
function ($window, $rootScope, $tooltip) {
var bodyEl = angular.element($window.document.body);
var matchesSelector = Element.prototype.matchesSelector || Element.prototype.webkitMatchesSelector || Element.prototype.mozMatchesSelector || Element.prototype.msMatchesSelector || Element.prototype.oMatchesSelector;
function DropdownFactory(element, config) {
var $dropdown = {};
// Common vars
var options = angular.extend({}, defaults, config);
var scope = $dropdown.$scope = options.scope && options.scope.$new() || $rootScope.$new();
$dropdown = $tooltip(element, options);
// Protected methods
$dropdown.$onKeyDown = function (evt) {
if (!/(38|40)/.test(evt.keyCode))
return;
evt.preventDefault();
evt.stopPropagation();
// Retrieve focused index
var items = angular.element($dropdown.$element[0].querySelectorAll('li:not(.divider) a'));
if (!items.length)
return;
var index;
angular.forEach(items, function (el, i) {
if (matchesSelector && matchesSelector.call(el, ':focus'))
index = i;
});
// Navigate with keyboard
if (evt.keyCode === 38 && index > 0)
index--;
else if (evt.keyCode === 40 && index < items.length - 1)
index++;
else if (angular.isUndefined(index))
index = 0;
items.eq(index)[0].focus();
};
// Overrides
var show = $dropdown.show;
$dropdown.show = function () {
show();
setTimeout(function () {
options.keyboard && $dropdown.$element.on('keydown', $dropdown.$onKeyDown);
bodyEl.on('click', onBodyClick);
});
};
var hide = $dropdown.hide;
$dropdown.hide = function () {
options.keyboard && $dropdown.$element.off('keydown', $dropdown.$onKeyDown);
bodyEl.off('click', onBodyClick);
hide();
};
// Private functions
function onBodyClick(evt) {
if (evt.target === element[0])
return;
return evt.target !== element[0] && $dropdown.hide();
}
return $dropdown;
}
return DropdownFactory;
}
];
}).directive('bsDropdown', [
'$window',
'$location',
'$sce',
'$dropdown',
function ($window, $location, $sce, $dropdown) {
return {
restrict: 'EAC',
scope: true,
link: function postLink(scope, element, attr, transclusion) {
// Directive options
var options = { scope: scope };
angular.forEach([
'placement',
'container',
'delay',
'trigger',
'keyboard',
'html',
'animation',
'template'
], function (key) {
if (angular.isDefined(attr[key]))
options[key] = attr[key];
});
// Support scope as an object
attr.bsDropdown && scope.$watch(attr.bsDropdown, function (newValue, oldValue) {
scope.content = newValue;
}, true);
// Initialize dropdown
var dropdown = $dropdown(element, options);
// Garbage collection
scope.$on('$destroy', function () {
dropdown.destroy();
options = null;
dropdown = null;
});
}
};
}
]);
// Source: date-parser.js
angular.module('mgcrea.ngStrap.helpers.dateParser', []).provider('$dateParser', [
'$localeProvider',
function ($localeProvider) {
var proto = Date.prototype;
function isNumeric(n) {
return !isNaN(parseFloat(n)) && isFinite(n);
}
var defaults = this.defaults = {
format: 'shortDate',
strict: false
};
this.$get = [
'$locale',
function ($locale) {
var DateParserFactory = function (config) {
var options = angular.extend({}, defaults, config);
var $dateParser = {};
var regExpMap = {
'sss': '[0-9]{3}',
'ss': '[0-5][0-9]',
's': options.strict ? '[1-5]?[0-9]' : '[0-9]|[0-5][0-9]',
'mm': '[0-5][0-9]',
'm': options.strict ? '[1-5]?[0-9]' : '[0-9]|[0-5][0-9]',
'HH': '[01][0-9]|2[0-3]',
'H': options.strict ? '1?[0-9]|2[0-3]' : '[01]?[0-9]|2[0-3]',
'hh': '[0][1-9]|[1][012]',
'h': options.strict ? '[1-9]|1[012]' : '0?[1-9]|1[012]',
'a': 'AM|PM',
'EEEE': $locale.DATETIME_FORMATS.DAY.join('|'),
'EEE': $locale.DATETIME_FORMATS.SHORTDAY.join('|'),
'dd': '0[1-9]|[12][0-9]|3[01]',
'd': options.strict ? '[1-9]|[1-2][0-9]|3[01]' : '0?[1-9]|[1-2][0-9]|3[01]',
'MMMM': $locale.DATETIME_FORMATS.MONTH.join('|'),
'MMM': $locale.DATETIME_FORMATS.SHORTMONTH.join('|'),
'MM': '0[1-9]|1[012]',
'M': options.strict ? '[1-9]|1[012]' : '0?[1-9]|1[012]',
'yyyy': '[1]{1}[0-9]{3}|[2]{1}[0-9]{3}',
'yy': '[0-9]{2}',
'y': options.strict ? '-?(0|[1-9][0-9]{0,3})' : '-?0*[0-9]{1,4}'
};
var setFnMap = {
'sss': proto.setMilliseconds,
'ss': proto.setSeconds,
's': proto.setSeconds,
'mm': proto.setMinutes,
'm': proto.setMinutes,
'HH': proto.setHours,
'H': proto.setHours,
'hh': proto.setHours,
'h': proto.setHours,
'dd': proto.setDate,
'd': proto.setDate,
'a': function (value) {
var hours = this.getHours();
return this.setHours(value.match(/pm/i) ? hours + 12 : hours);
},
'MMMM': function (value) {
return this.setMonth($locale.DATETIME_FORMATS.MONTH.indexOf(value));
},
'MMM': function (value) {
return this.setMonth($locale.DATETIME_FORMATS.SHORTMONTH.indexOf(value));
},
'MM': function (value) {
return this.setMonth(1 * value - 1);
},
'M': function (value) {
return this.setMonth(1 * value - 1);
},
'yyyy': proto.setFullYear,
'yy': function (value) {
return this.setFullYear(2000 + 1 * value);
},
'y': proto.setFullYear
};
var regex, setMap;
$dateParser.init = function () {
$dateParser.$format = $locale.DATETIME_FORMATS[options.format] || options.format;
regex = regExpForFormat($dateParser.$format);
setMap = setMapForFormat($dateParser.$format);
};
$dateParser.isValid = function (date) {
if (angular.isDate(date))
return !isNaN(date.getTime());
return regex.test(date);
};
$dateParser.parse = function (value, baseDate)