blocks inside then you do not have
// to specify the language for each block
var language = _attr(block, 'data-language') || _attr(block.parentNode, 'data-language');
// this adds support for specifying language via a css class
// you can use the Google Code Prettify style:
// or the HTML5 style:
if (!language) {
var pattern = /\blang(?:uage)?-(\w+)/,
match = block.className.match(pattern) || block.parentNode.className.match(pattern);
if (match) {
language = match[1];
}
}
return language;
}
/**
* makes sure html entities are always used for tags
*
* @param {string} code
* @returns {string}
*/
function _htmlEntities(code) {
return code.replace(//g, '>').replace(/&(?![\w\#]+;)/g, '&');
}
/**
* determines if a new match intersects with an existing one
*
* @param {number} start1 start position of existing match
* @param {number} end1 end position of existing match
* @param {number} start2 start position of new match
* @param {number} end2 end position of new match
* @returns {boolean}
*/
function _intersects(start1, end1, start2, end2) {
if (start2 >= start1 && start2 < end1) {
return true;
}
return end2 > start1 && end2 < end1;
}
/**
* determines if two different matches have complete overlap with each other
*
* @param {number} start1 start position of existing match
* @param {number} end1 end position of existing match
* @param {number} start2 start position of new match
* @param {number} end2 end position of new match
* @returns {boolean}
*/
function _hasCompleteOverlap(start1, end1, start2, end2) {
// if the starting and end positions are exactly the same
// then the first one should stay and this one should be ignored
if (start2 == start1 && end2 == end1) {
return false;
}
return start2 <= start1 && end2 >= end1;
}
/**
* determines if the match passed in falls inside of an existing match
* this prevents a regex pattern from matching inside of a bigger pattern
*
* @param {number} start - start position of new match
* @param {number} end - end position of new match
* @returns {boolean}
*/
function _matchIsInsideOtherMatch(start, end) {
for (var key in replacement_positions[CURRENT_LEVEL]) {
key = parseInt(key, 10);
// if this block completely overlaps with another block
// then we should remove the other block and return false
if (_hasCompleteOverlap(key, replacement_positions[CURRENT_LEVEL][key], start, end)) {
delete replacement_positions[CURRENT_LEVEL][key];
delete replacements[CURRENT_LEVEL][key];
}
if (_intersects(key, replacement_positions[CURRENT_LEVEL][key], start, end)) {
return true;
}
}
return false;
}
/**
* takes a string of code and wraps it in a span tag based on the name
*
* @param {string} name name of the pattern (ie keyword.regex)
* @param {string} code block of code to wrap
* @returns {string}
*/
function _wrapCodeInSpan(name, code) {
return '' + code + '';
}
/**
* finds out the position of group match for a regular expression
*
* @see http://stackoverflow.com/questions/1985594/how-to-find-index-of-groups-in-match
*
* @param {Object} match
* @param {number} group_number
* @returns {number}
*/
function _indexOfGroup(match, group_number) {
var index = 0,
i;
for (i = 1; i < group_number; ++i) {
if (match[i]) {
index += match[i].length;
}
}
return index;
}
/**
* matches a regex pattern against a block of code
* finds all matches that should be processed and stores the positions
* of where they should be replaced within the string
*
* this is where pretty much all the work is done but it should not
* be called directly
*
* @param {RegExp} pattern
* @param {string} code
* @returns void
*/
function _processPattern(regex, pattern, code, callback)
{
var match = regex.exec(code);
if (!match) {
return callback();
}
++match_counter;
// treat match 0 the same way as name
if (!pattern['name'] && typeof pattern['matches'][0] == 'string') {
pattern['name'] = pattern['matches'][0];
delete pattern['matches'][0];
}
var replacement = match[0],
start_pos = match.index,
end_pos = match[0].length + start_pos,
/**
* callback to process the next match of this pattern
*/
processNext = function() {
var nextCall = function() {
_processPattern(regex, pattern, code, callback);
};
// every 50 items we process let's call set timeout
// to let the ui breathe a little
return match_counter % 50 > 0 ? nextCall() : setTimeout(nextCall, 0);
};
// if this is not a child match and it falls inside of another
// match that already happened we should skip it and continue processing
if (_matchIsInsideOtherMatch(start_pos, end_pos)) {
return processNext();
}
/**
* callback for when a match was successfully processed
*
* @param {string} replacement
* @returns void
*/
var onMatchSuccess = function(replacement) {
// if this match has a name then wrap it in a span tag
if (pattern['name']) {
replacement = _wrapCodeInSpan(pattern['name'], replacement);
}
// console.log('LEVEL', CURRENT_LEVEL, 'replace', match[0], 'with', replacement, 'at position', start_pos, 'to', end_pos);
// store what needs to be replaced with what at this position
if (!replacements[CURRENT_LEVEL]) {
replacements[CURRENT_LEVEL] = {};
replacement_positions[CURRENT_LEVEL] = {};
}
replacements[CURRENT_LEVEL][start_pos] = {
'replace': match[0],
'with': replacement
};
// store the range of this match so we can use it for comparisons
// with other matches later
replacement_positions[CURRENT_LEVEL][start_pos] = end_pos;
// process the next match
processNext();
},
// if this pattern has sub matches for different groups in the regex
// then we should process them one at a time by rerunning them through
// this function to generate the new replacement
//
// we run through them backwards because the match position of earlier
// matches will not change depending on what gets replaced in later
// matches
group_keys = keys(pattern['matches']),
/**
* callback for processing a sub group
*
* @param {number} i
* @param {Array} group_keys
* @param {Function} callback
*/
processGroup = function(i, group_keys, callback) {
if (i >= group_keys.length) {
return callback(replacement);
}
var processNextGroup = function() {
processGroup(++i, group_keys, callback);
},
block = match[group_keys[i]];
// if there is no match here then move on
if (!block) {
return processNextGroup();
}
var group = pattern['matches'][group_keys[i]],
language = group['language'],
/**
* process group is what group we should use to actually process
* this match group
*
* for example if the subgroup pattern looks like this
* 2: {
* 'name': 'keyword',
* 'pattern': /true/g
* }
*
* then we use that as is, but if it looks like this
*
* 2: {
* 'name': 'keyword',
* 'matches': {
* 'name': 'special',
* 'pattern': /whatever/g
* }
* }
*
* we treat the 'matches' part as the pattern and keep
* the name around to wrap it with later
*/
process_group = group['name'] && group['matches'] ? group['matches'] : group,
/**
* takes the code block matched at this group, replaces it
* with the highlighted block, and optionally wraps it with
* a span with a name
*
* @param {string} block
* @param {string} replace_block
* @param {string|null} match_name
*/
_replaceAndContinue = function(block, replace_block, match_name) {
replacement = _replaceAtPosition(_indexOfGroup(match, group_keys[i]), block, match_name ? _wrapCodeInSpan(match_name, replace_block) : replace_block, replacement);
processNextGroup();
};
// if this is a sublanguage go and process the block using that language
if (language) {
return _highlightBlockForLanguage(block, language, function(code) {
_replaceAndContinue(block, code);
});
}
// if this is a string then this match is directly mapped to selector
// so all we have to do is wrap it in a span and continue
if (typeof group === 'string') {
return _replaceAndContinue(block, block, group);
}
// the process group can be a single pattern or an array of patterns
// _processCodeWithPatterns always expects an array so we convert it here
_processCodeWithPatterns(block, process_group.length ? process_group : [process_group], function(code) {
_replaceAndContinue(block, code, group['matches'] ? group['name'] : 0);
});
};
processGroup(0, group_keys, onMatchSuccess);
}
/**
* should a language bypass the default patterns?
*
* if you call Rainbow.extend() and pass true as the third argument
* it will bypass the defaults
*/
function _bypassDefaultPatterns(language)
{
return bypass_defaults[language];
}
/**
* returns a list of regex patterns for this language
*
* @param {string} language
* @returns {Array}
*/
function _getPatternsForLanguage(language) {
var patterns = language_patterns[language] || [],
default_patterns = language_patterns[DEFAULT_LANGUAGE] || [];
return _bypassDefaultPatterns(language) ? patterns : patterns.concat(default_patterns);
}
/**
* substring replace call to replace part of a string at a certain position
*
* @param {number} position the position where the replacement should happen
* @param {string} replace the text we want to replace
* @param {string} replace_with the text we want to replace it with
* @param {string} code the code we are doing the replacing in
* @returns {string}
*/
function _replaceAtPosition(position, replace, replace_with, code) {
var sub_string = code.substr(position);
return code.substr(0, position) + sub_string.replace(replace, replace_with);
}
/**
* sorts an object by index descending
*
* @param {Object} object
* @return {Array}
*/
function keys(object) {
var locations = [],
replacement,
pos;
for(var location in object) {
if (object.hasOwnProperty(location)) {
locations.push(location);
}
}
// numeric descending
return locations.sort(function(a, b) {
return b - a;
});
}
/**
* processes a block of code using specified patterns
*
* @param {string} code
* @param {Array} patterns
* @returns void
*/
function _processCodeWithPatterns(code, patterns, callback)
{
// we have to increase the level here so that the
// replacements will not conflict with each other when
// processing sub blocks of code
++CURRENT_LEVEL;
// patterns are processed one at a time through this function
function _workOnPatterns(patterns, i)
{
// still have patterns to process, keep going
if (i < patterns.length) {
return _processPattern(patterns[i]['pattern'], patterns[i], code, function() {
_workOnPatterns(patterns, ++i);
});
}
// we are done processing the patterns
// process the replacements and update the DOM
_processReplacements(code, function(code) {
// when we are done processing replacements
// we are done at this level so we can go back down
delete replacements[CURRENT_LEVEL];
delete replacement_positions[CURRENT_LEVEL];
--CURRENT_LEVEL;
callback(code);
});
}
_workOnPatterns(patterns, 0);
}
/**
* process replacements in the string of code to actually update the markup
*
* @param {string} code the code to process replacements in
* @param {Function} onComplete what to do when we are done processing
* @returns void
*/
function _processReplacements(code, onComplete) {
/**
* processes a single replacement
*
* @param {string} code
* @param {Array} positions
* @param {number} i
* @param {Function} onComplete
* @returns void
*/
function _processReplacement(code, positions, i, onComplete) {
if (i < positions.length) {
++replacement_counter;
var pos = positions[i],
replacement = replacements[CURRENT_LEVEL][pos];
code = _replaceAtPosition(pos, replacement['replace'], replacement['with'], code);
// process next function
var next = function() {
_processReplacement(code, positions, ++i, onComplete);
};
// use a timeout every 250 to not freeze up the UI
return replacement_counter % 250 > 0 ? next() : setTimeout(next, 0);
}
onComplete(code);
}
var string_positions = keys(replacements[CURRENT_LEVEL]);
_processReplacement(code, string_positions, 0, onComplete);
}
/**
* takes a string of code and highlights it according to the language specified
*
* @param {string} code
* @param {string} language
* @param {Function} onComplete
* @returns void
*/
function _highlightBlockForLanguage(code, language, onComplete) {
var patterns = _getPatternsForLanguage(language);
_processCodeWithPatterns(_htmlEntities(code), patterns, onComplete);
}
/**
* highlight an individual code block
*
* @param {Array} code_blocks
* @param {number} i
* @returns void
*/
function _highlightCodeBlock(code_blocks, i, onComplete) {
if (i < code_blocks.length) {
var block = code_blocks[i],
language = _getLanguageForBlock(block);
if (!_hasClass(block, 'rainbow') && language) {
language = language.toLowerCase();
_addClass(block, 'rainbow');
return _highlightBlockForLanguage(block.innerHTML, language, function(code) {
block.innerHTML = code;
// reset the replacement arrays
replacements = {};
replacement_positions = {};
// if you have a listener attached tell it that this block is now highlighted
if (onHighlight) {
onHighlight(block, language);
}
// process the next block
setTimeout(function() {
_highlightCodeBlock(code_blocks, ++i, onComplete);
}, 0);
});
}
return _highlightCodeBlock(code_blocks, ++i, onComplete);
}
if (onComplete) {
onComplete();
}
}
/**
* start highlighting all the code blocks
*
* @returns void
*/
function _highlight(node, onComplete) {
// the first argument can be an Event or a DOM Element
// I was originally checking instanceof Event but that makes it break
// when using mootools
//
// @see https://github.com/ccampbell/rainbow/issues/32
//
node = node && typeof node.getElementsByTagName == 'function' ? node : document;
var pre_blocks = node.getElementsByTagName('pre'),
code_blocks = node.getElementsByTagName('code'),
i,
final_blocks = [];
// @see http://stackoverflow.com/questions/2735067/how-to-convert-a-dom-node-list-to-an-array-in-javascript
// we are going to process all blocks
for (i = 0; i < code_blocks.length; ++i) {
final_blocks.push(code_blocks[i]);
}
// loop through the pre blocks to see which ones we should add
for (i = 0; i < pre_blocks.length; ++i) {
// if the pre block has no code blocks then process it directly
if (!pre_blocks[i].getElementsByTagName('code').length) {
final_blocks.push(pre_blocks[i]);
}
}
_highlightCodeBlock(final_blocks, 0, onComplete);
}
/**
* public methods
*/
return {
/**
* extends the language pattern matches
*
* @param {*} language name of language
* @param {*} patterns array of patterns to add on
* @param {boolean|null} bypass if true this will bypass the default language patterns
*/
extend: function(language, patterns, bypass) {
// if there is only one argument then we assume that we want to
// extend the default language rules
if (arguments.length == 1) {
patterns = language;
language = DEFAULT_LANGUAGE;
}
bypass_defaults[language] = bypass;
language_patterns[language] = patterns.concat(language_patterns[language] || []);
},
/**
* call back to let you do stuff in your app after a piece of code has been highlighted
*
* @param {Function} callback
*/
onHighlight: function(callback) {
onHighlight = callback;
},
/**
* method to set a global class that will be applied to all spans
*
* @param {string} class_name
*/
addClass: function(class_name) {
global_class = class_name;
},
/**
* starts the magic rainbow
*
* @returns void
*/
color: function() {
// if you want to straight up highlight a string you can pass the string of code,
// the language, and a callback function
if (typeof arguments[0] == 'string') {
return _highlightBlockForLanguage(arguments[0], arguments[1], arguments[2]);
}
// if you pass a callback function then we rerun the color function
// on all the code and call the callback function on complete
if (typeof arguments[0] == 'function') {
return _highlight(0, arguments[0]);
}
// otherwise we use whatever node you passed in with an optional
// callback function as the second parameter
_highlight(arguments[0], arguments[1]);
}
};
}) ();
/**
* adds event listener to start highlighting
*/
(function() {
if (window.addEventListener) {
return window.addEventListener('load', Rainbow.color, false);
}
window.attachEvent('onload', Rainbow.color);
}) ();
// When using Google closure compiler in advanced mode some methods
// get renamed. This keeps a public reference to these methods so they can
// still be referenced from outside this library.
Rainbow["onHighlight"] = Rainbow.onHighlight;
Rainbow["addClass"] = Rainbow.addClass;
================================================
FILE: misc/demo/assets/smoothscroll-angular-custom.js
================================================
/*
* https://github.com/alicelieutier/smoothScroll/
* A teeny tiny, standard compliant, smooth scroll script with ease-in-out effect and no jQuery (or any other dependancy, FWIW).
* MIT License
*/
window.smoothScroll = (function(){
// We do not want this script to be applied in browsers that do not support those
// That means no smoothscroll on IE9 and below.
if(document.querySelectorAll === void 0 || window.pageYOffset === void 0 || history.pushState === void 0) { return; }
// Get the top position of an element in the document
var getTop = function(element) {
// return value of html.getBoundingClientRect().top ... IE : 0, other browsers : -pageYOffset
if(element.nodeName === 'HTML') return -window.pageYOffset
return element.getBoundingClientRect().top + window.pageYOffset;
}
// ease in out function thanks to:
// http://blog.greweb.fr/2012/02/bezier-curve-based-easing-functions-from-concept-to-implementation/
var easeInOutCubic = function (t) { return t<.5 ? 4*t*t*t : (t-1)*(2*t-2)*(2*t-2)+1 }
// calculate the scroll position we should be in
// given the start and end point of the scroll
// the time elapsed from the beginning of the scroll
// and the total duration of the scroll (default 500ms)
var position = function(start, end, elapsed, duration) {
if (elapsed > duration) return end;
return start + (end - start) * easeInOutCubic(elapsed / duration); // <-- you can change the easing funtion there
// return start + (end - start) * (elapsed / duration); // <-- this would give a linear scroll
}
// we use requestAnimationFrame to be called by the browser before every repaint
// if the first argument is an element then scroll to the top of this element
// if the first argument is numeric then scroll to this location
// if the callback exist, it is called when the scrolling is finished
var smoothScroll = function(el, duration, callback){
duration = duration || 500;
var start = window.pageYOffset;
if (typeof el === 'number') {
var end = parseInt(el);
} else {
var end = getTop(el);
}
var clock = Date.now();
var requestAnimationFrame = window.requestAnimationFrame ||
window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame ||
function(fn){window.setTimeout(fn, 15);};
var step = function(){
var elapsed = Date.now() - clock;
window.scroll(0, position(start, end, elapsed, duration));
if (elapsed > duration) {
if (typeof callback === 'function') {
callback(el);
}
} else {
requestAnimationFrame(step);
}
}
step();
}
var linkHandler = function(ev) {
ev.preventDefault();
if (location.hash !== this.hash) {
//NOTE(@ajoslin): Changed this line to stop $digest errors
//window.history.pushState(null, null, this.hash)
angular.element(document).injector().get('$location').hash(this.hash);
}
// using the history api to solve issue #1 - back doesn't work
// most browser don't update :target when the history api is used:
// THIS IS A BUG FROM THE BROWSERS.
// change the scrolling duration in this call
var targetEl = document.getElementById(this.hash.substring(1));
if (targetEl) {
smoothScroll(document.getElementById(this.hash.substring(1)), 500, function(el) {
location.replace('#' + el.id)
// this will cause the :target to be activated.
});
}
}
// We look for all the internal links in the documents and attach the smoothscroll function
document.addEventListener("DOMContentLoaded", function () {
var internal = document.querySelectorAll('a[href^="#"]'), a;
for(var i=internal.length; a=internal[--i];){
a.addEventListener("click", linkHandler, false);
}
});
// return smoothscroll API
return smoothScroll;
})();
================================================
FILE: misc/demo/assets/uglifyjs.js
================================================
!function(n,e){"use strict";function t(n){for(var e=Object.create(null),t=0;t=0;)if(e[t]==n)return!0;return!1}function a(n,e){for(var t=0,r=e.length;r>t;++t)if(n(e[t]))return e[t]}function u(n,e){if(0>=e)return"";if(1==e)return n;var t=u(n,e>>1);return t+=t,1&e&&(t+=n),t}function s(n,e){Error.call(this,n),this.msg=n,this.defs=e}function c(n,e,t){n===!0&&(n={});var r=n||{};if(t)for(var i in r)r.hasOwnProperty(i)&&!e.hasOwnProperty(i)&&s.croak("`"+i+"` is not a supported option",e);for(var i in e)e.hasOwnProperty(i)&&(r[i]=n&&n.hasOwnProperty(i)?n[i]:e[i]);return r}function f(n,e){for(var t in e)e.hasOwnProperty(t)&&(n[t]=e[t]);return n}function l(){}function p(n,e){n.indexOf(e)<0&&n.push(e)}function d(n,e){return n.replace(/\{(.+?)\}/g,function(n,t){return e[t]})}function h(n,e){for(var t=n.length;--t>=0;)n[t]===e&&n.splice(t,1)}function _(n,e){function t(n,t){for(var r=[],i=0,o=0,a=0;i=0})}function g(n){function e(n){if(1==n.length)return t+="return str === "+JSON.stringify(n[0])+";";t+="switch(str){";for(var e=0;e3){r.sort(function(n,e){return e.length-n.length}),t+="switch(str.length){";for(var i=0;i=0;)if(!e(n[t]))return!1;return!0}function y(){this._values=Object.create(null),this._size=0}function A(n,e,t,r){arguments.length<4&&(r=W),e=e?e.split(/\s+/):[];var i=e;r&&r.PROPS&&(e=e.concat(r.PROPS));for(var o="return function AST_"+n+"(props){ if (props) { ",a=e.length;--a>=0;)o+="this."+e[a]+" = props."+e[a]+";";var u=r&&new r;(u&&u.initialize||t&&t.initialize)&&(o+="this.initialize();"),o+="}}";var s=new Function(o)();if(u&&(s.prototype=u,s.BASE=r),r&&r.SUBCLASSES.push(s),s.prototype.CTOR=s,s.PROPS=e||null,s.SELF_PROPS=i,s.SUBCLASSES=[],n&&(s.prototype.TYPE=s.TYPE=n),t)for(a in t)t.hasOwnProperty(a)&&(/^\$/.test(a)?s[a.substr(1)]=t[a]:s.prototype[a]=t[a]);return s.DEFMETHOD=function(n,e){this.prototype[n]=e},s}function w(n,e){n.body instanceof Y?n.body._walk(e):n.body.forEach(function(n){n._walk(e)})}function E(n){this.visit=n,this.stack=[]}function D(n){return n>=97&&122>=n||n>=65&&90>=n||n>=170&&qt.letter.test(String.fromCharCode(n))}function F(n){return n>=48&&57>=n}function S(n){return F(n)||D(n)}function C(n){return qt.non_spacing_mark.test(n)||qt.space_combining_mark.test(n)}function k(n){return qt.connector_punctuation.test(n)}function x(n){return!St(n)&&/^[a-z_$][a-z0-9_$]*$/i.test(n)}function B(n){return 36==n||95==n||D(n)}function T(n){var e=n.charCodeAt(0);return B(e)||F(e)||8204==e||8205==e||C(n)||k(n)}function $(n){return/^[a-z_$][a-z0-9_$]*$/i.test(n)}function O(n){return xt.test(n)?parseInt(n.substr(2),16):Bt.test(n)?parseInt(n.substr(1),8):Tt.test(n)?parseFloat(n):void 0}function M(n,e,t,r){this.message=n,this.line=e,this.col=t,this.pos=r,this.stack=(new Error).stack}function N(n,e,t,r,i){throw new M(n,t,r,i)}function R(n,e,t){return n.type==e&&(null==t||n.value==t)}function q(n,e,t){function r(){return D.text.charAt(D.pos)}function i(n,e){var t=D.text.charAt(D.pos++);if(n&&!t)throw Ht;return"\n"==t?(D.newline_before=D.newline_before||!e,++D.line,D.col=0):++D.col,t}function o(n){for(;n-->0;)i()}function a(n){return D.text.substr(D.pos,n.length)==n}function u(n,e){var t=D.text.indexOf(n,D.pos);if(e&&-1==t)throw Ht;return t}function s(){D.tokline=D.line,D.tokcol=D.col,D.tokpos=D.pos}function c(n,t,r){D.regex_allowed="operator"==n&&!Pt(t)||"keyword"==n&&Ct(t)||"punc"==n&&Mt(t),C="punc"==n&&"."==t;var i={type:n,value:t,line:D.tokline,col:D.tokcol,pos:D.tokpos,endpos:D.pos,nlb:D.newline_before,file:e};if(!r){i.comments_before=D.comments_before,D.comments_before=[];for(var o=0,a=i.comments_before.length;a>o;o++)i.nlb=i.nlb||i.comments_before[o].nlb}return D.newline_before=!1,new L(i)}function f(){for(;Ot(r());)i()}function l(n){for(var e,t="",o=0;(e=r())&&n(e,o++);)t+=i();return t}function p(n){N(n,e,D.tokline,D.tokcol,D.tokpos)}function d(n){var e=!1,t=!1,r=!1,i="."==n,o=l(function(o,a){var u=o.charCodeAt(0);switch(u){case 120:case 88:return r?!1:r=!0;case 101:case 69:return r?!0:e?!1:e=t=!0;case 45:return t||0==a&&!n;case 43:return t;case t=!1,46:return i||r||e?!1:i=!0}return S(u)});n&&(o=n+o);var a=O(o);return isNaN(a)?void p("Invalid syntax: "+o):c("num",a)}function h(n){var e=i(!0,n);switch(e.charCodeAt(0)){case 110:return"\n";case 114:return"\r";case 116:return" ";case 98:return"\b";case 118:return"";case 102:return"\f";case 48:return"\x00";case 120:return String.fromCharCode(_(2));case 117:return String.fromCharCode(_(4));case 10:return"";default:return e}}function _(n){for(var e=0;n>0;--n){var t=parseInt(i(!0),16);isNaN(t)&&p("Invalid hex-character pattern in string"),e=e<<4|t}return e}function m(n){var e,t=D.regex_allowed,r=u("\n");return-1==r?(e=D.text.substr(D.pos),D.pos=D.text.length):(e=D.text.substring(D.pos,r),D.pos=r),D.comments_before.push(c(n,e,!0)),D.regex_allowed=t,E()}function v(){for(var n,e,t=!1,o="",a=!1;null!=(n=r());)if(t)"u"!=n&&p("Expecting UnicodeEscapeSequence -- uXXXX"),n=h(),T(n)||p("Unicode char: "+n.charCodeAt(0)+" is not valid in identifier"),o+=n,t=!1;else if("\\"==n)a=t=!0,i();else{if(!T(n))break;o+=i()}return Dt(o)&&a&&(e=o.charCodeAt(0).toString(16).toUpperCase(),o="\\u"+"0000".substr(e.length)+e+o.slice(1)),o}function g(n){function e(n){if(!r())return n;var t=n+r();return $t(t)?(i(),e(t)):n}return c("operator",e(n||i()))}function b(){switch(i(),r()){case"/":return i(),m("comment1");case"*":return i(),x()}return D.regex_allowed?$(""):g("/")}function y(){return i(),F(r().charCodeAt(0))?d("."):c("punc",".")}function A(){var n=v();return C?c("name",n):Ft(n)?c("atom",n):Dt(n)?$t(n)?c("operator",n):c("keyword",n):c("name",n)}function w(n,e){return function(t){try{return e(t)}catch(r){if(r!==Ht)throw r;p(n)}}}function E(n){if(null!=n)return $(n);if(f(),s(),t){if(a("")&&D.newline_before)return o(3),m("comment4")}var e=r();if(!e)return c("eof");var u=e.charCodeAt(0);switch(u){case 34:case 39:return k();case 46:return y();case 47:return b()}return F(u)?d():Nt(e)?c("punc",i()):kt(e)?g():92==u||B(u)?A():void p("Unexpected character '"+e+"'")}var D={text:n.replace(/\r\n?|[\n\u2028\u2029]/g,"\n").replace(/\uFEFF/g,""),filename:e,pos:0,tokpos:0,line:1,tokline:0,col:0,tokcol:0,newline_before:!1,regex_allowed:!1,comments_before:[]},C=!1,k=w("Unterminated string constant",function(){for(var n=i(),e="";;){var t=i(!0);if("\\"==t){var r=0,o=null;t=l(function(n){if(n>="0"&&"7">=n){if(!o)return o=n,++r;if("3">=o&&2>=r)return++r;if(o>="4"&&1>=r)return++r}return!1}),t=r>0?String.fromCharCode(parseInt(t,8)):h(!0)}else if(t==n)break;e+=t}return c("string",e)}),x=w("Unterminated multiline comment",function(){var n=D.regex_allowed,e=u("*/",!0),t=D.text.substring(D.pos,e),r=t.split("\n"),i=r.length;D.pos=e+2,D.line+=i-1,i>1?D.col=r[i-1].length:D.col+=r[i-1].length,D.col+=2;var o=D.newline_before=D.newline_before||t.indexOf("\n")>=0;return D.comments_before.push(c("comment2",t,!0)),D.regex_allowed=n,D.newline_before=o,E()}),$=w("Unterminated regular expression",function(n){for(var e,t=!1,r=!1;e=i(!0);)if(t)n+="\\"+e,t=!1;else if("["==e)r=!0,n+=e;else if("]"==e&&r)r=!1,n+=e;else{if("/"==e&&!r)break;"\\"==e?t=!0:n+=e}var o=v();return c("regexp",new RegExp(n,o))});return E.context=function(n){return n&&(D=n),D},E}function H(n,e){function t(n,e){return R(I.token,n,e)}function r(){return I.peeked||(I.peeked=I.input())}function i(){return I.prev=I.token,I.peeked?(I.token=I.peeked,I.peeked=null):I.token=I.input(),I.in_directives=I.in_directives&&("string"==I.token.type||t("punc",";")),I.token}function o(){return I.prev}function u(n,e,t,r){var i=I.input.context();N(n,i.filename,null!=e?e:i.tokline,null!=t?t:i.tokcol,null!=r?r:i.tokpos)}function s(n,e){u(e,n.line,n.col)}function f(n){null==n&&(n=I.token),s(n,"Unexpected token: "+n.type+" ("+n.value+")")}function l(n,e){return t(n,e)?i():void s(I.token,"Unexpected token "+I.token.type+" «"+I.token.value+"», expected "+n+" «"+e+"»")}function p(n){return l("punc",n)}function d(){return!e.strict&&(I.token.nlb||t("eof")||t("punc","}"))}function h(){t("punc",";")?i():d()||f()}function _(){p("(");var n=De(!0);return p(")"),n}function m(n){return function(){var e=I.token,t=n(),r=o();return t.start=e,t.end=r,t}}function v(){(t("operator","/")||t("operator","/="))&&(I.peeked=null,I.token=I.input(I.token.value.substr(1)))}function g(){var n=M(ut);a(function(e){return e.name==n.name},I.labels)&&u("Label "+n.name+" defined twice"),p(":"),I.labels.push(n);var e=U();return I.labels.pop(),e instanceof te||n.references.forEach(function(e){e instanceof Ae&&(e=e.label.start,u("Continue label `"+n.name+"` refers to non-IterationStatement.",e.line,e.col,e.pos))}),new ee({body:e,label:n})}function b(n){return new K({body:(n=De(!0),h(),n)})}function y(n){var e,t=null;d()||(t=M(ct,!0)),null!=t?(e=a(function(n){return n.name==t.name},I.labels),e||u("Undefined label "+t.name),t.thedef=e):0==I.in_loop&&u(n.TYPE+" not inside a loop or switch"),h();var r=new n({label:t});return e&&e.references.push(r),r}function A(){p("(");var n=null;return!t("punc",";")&&(n=t("keyword","var")?(i(),L(!0)):De(!0,!0),t("operator","in"))?(n instanceof Te&&n.definitions.length>1&&u("Only one variable declaration allowed in for..in loop"),i(),E(n)):w(n)}function w(n){p(";");var e=t("punc",";")?null:De(!0);p(";");var r=t("punc",")")?null:De(!0);return p(")"),new ae({init:n,condition:e,step:r,body:j(U)})}function E(n){var e=n instanceof Te?n.definitions[0].name:null,t=De(!0);return p(")"),new ue({init:n,name:e,object:t,body:j(U)})}function D(){var n=_(),e=U(),r=null;return t("keyword","else")&&(i(),r=U()),new we({condition:n,body:e,alternative:r})}function F(){p("{");for(var n=[];!t("punc","}");)t("eof")&&f(),n.push(U());return i(),n}function S(){p("{");for(var n,e=[],r=null,a=null;!t("punc","}");)t("eof")&&f(),t("keyword","case")?(a&&(a.end=o()),r=[],a=new Se({start:(n=I.token,i(),n),expression:De(!0),body:r}),e.push(a),p(":")):t("keyword","default")?(a&&(a.end=o()),r=[],a=new Fe({start:(n=I.token,i(),p(":"),n),body:r}),e.push(a)):(r||f(),r.push(U()));return a&&(a.end=o()),i(),e}function C(){var n=F(),e=null,r=null;if(t("keyword","catch")){var a=I.token;i(),p("(");var s=M(at);p(")"),e=new ke({start:a,argname:s,body:F(),end:o()})}if(t("keyword","finally")){var a=I.token;i(),r=new xe({start:a,body:F(),end:o()})}return e||r||u("Missing catch/finally blocks"),new Ce({body:n,bcatch:e,bfinally:r})}function k(n,e){for(var r=[];r.push(new Oe({start:I.token,name:M(e?tt:et),value:t("operator","=")?(i(),De(!1,n)):null,end:o()})),t("punc",",");)i();return r}function x(){var n,e=I.token;switch(e.type){case"name":case"keyword":n=O(st);break;case"num":n=new dt({start:e,end:e,value:e.value});break;case"string":n=new pt({start:e,end:e,value:e.value});break;case"regexp":n=new ht({start:e,end:e,value:e.value});break;case"atom":switch(e.value){case"false":n=new wt({start:e,end:e});break;case"true":n=new Et({start:e,end:e});break;case"null":n=new mt({start:e,end:e})}}return i(),n}function B(n,e,r){for(var o=!0,a=[];!t("punc",n)&&(o?o=!1:p(","),!e||!t("punc",n));)a.push(t("punc",",")&&r?new bt({start:I.token,end:I.token}):De(!1));return i(),a}function T(){var n=I.token;switch(i(),n.type){case"num":case"string":case"name":case"operator":case"keyword":case"atom":return n.value;default:f()}}function $(){var n=I.token;switch(i(),n.type){case"name":case"operator":case"keyword":case"atom":return n.value;default:f()}}function O(n){var e=I.token.value;return new("this"==e?ft:n)({name:String(e),start:I.token,end:I.token})}function M(n,e){if(!t("name"))return e||u("Name expected"),null;var r=O(n);return i(),r}function H(n,e,t){return"++"!=e&&"--"!=e||P(t)||u("Invalid use of "+e+" operator"),new n({operator:e,expression:t})}function z(n){return _e(le(!0),0,n)}function P(n){return e.strict?n instanceof ft?!1:n instanceof qe||n instanceof Ze:!0}function j(n){++I.in_loop;var e=n();return--I.in_loop,e}e=c(e,{strict:!1,filename:null,toplevel:null,expression:!1,html5_comments:!0});var I={input:"string"==typeof n?q(n,e.filename,e.html5_comments):n,token:null,prev:null,peeked:null,in_function:0,in_directives:!0,in_loop:0,labels:[]};I.token=i();var U=m(function(){var n;switch(v(),I.token.type){case"string":var e=I.in_directives,a=b();return e&&a.body instanceof pt&&!t("punc",",")?new G({value:a.body.value}):a;case"num":case"regexp":case"operator":case"atom":return b();case"name":return R(r(),"punc",":")?g():b();case"punc":switch(I.token.value){case"{":return new Z({start:I.token,body:F(),end:o()});case"[":case"(":return b();case";":return i(),new Q;default:f()}case"keyword":switch(n=I.token.value,i(),n){case"break":return y(ye);case"continue":return y(Ae);case"debugger":return h(),new X;case"do":return new ie({body:j(U),condition:(l("keyword","while"),n=_(),h(),n)});case"while":return new oe({condition:_(),body:j(U)});case"for":return A();case"function":return V(he);case"if":return D();case"return":return 0==I.in_function&&u("'return' outside of function"),new ve({value:t("punc",";")?(i(),null):d()?null:(n=De(!0),h(),n)});case"switch":return new Ee({expression:_(),body:j(S)});case"throw":return I.token.nlb&&u("Illegal newline after 'throw'"),new ge({value:(n=De(!0),h(),n)});case"try":return C();case"var":return n=L(),h(),n;case"const":return n=W(),h(),n;case"with":return new se({expression:_(),body:U()});default:f()}}}),V=function(n){var e=n===he,r=t("name")?M(e?it:ot):null;return e&&!r&&f(),p("("),new n({name:r,argnames:function(n,e){for(;!t("punc",")");)n?n=!1:p(","),e.push(M(rt));return i(),e}(!0,[]),body:function(n,e){++I.in_function,I.in_directives=!0,I.in_loop=0,I.labels=[];var t=F();return--I.in_function,I.in_loop=n,I.labels=e,t}(I.in_loop,I.labels)})},L=function(n){return new Te({start:o(),definitions:k(n,!1),end:o()})},W=function(){return new $e({start:o(),definitions:k(!1,!0),end:o()})},Y=function(){var n=I.token;l("operator","new");var e,r=J(!1);return t("punc","(")?(i(),e=B(")")):e=[],ce(new Ne({start:n,expression:r,args:e,end:o()}),!0)},J=function(n){if(t("operator","new"))return Y();var e=I.token;if(t("punc")){switch(e.value){case"(":i();var r=De(!0);return r.start=e,r.end=I.token,p(")"),ce(r,n);case"[":return ce(ne(),n);case"{":return ce(re(),n)}f()}if(t("keyword","function")){i();var a=V(de);return a.start=e,a.end=o(),ce(a,n)}return Vt[I.token.type]?ce(x(),n):void f()},ne=m(function(){return p("["),new We({elements:B("]",!e.strict,!0)})}),re=m(function(){p("{");for(var n=!0,r=[];!t("punc","}")&&(n?n=!1:p(","),e.strict||!t("punc","}"));){var a=I.token,u=a.type,s=T();if("name"==u&&!t("punc",":")){if("get"==s){r.push(new Je({start:a,key:x(),value:V(pe),end:o()}));continue}if("set"==s){r.push(new Ke({start:a,key:x(),value:V(pe),end:o()}));continue}}p(":"),r.push(new Ge({start:a,key:s,value:De(!1),end:o()}))}return i(),new Ye({properties:r})}),ce=function(n,e){var r=n.start;if(t("punc","."))return i(),ce(new He({start:r,expression:n,property:$(),end:o()}),e);if(t("punc","[")){i();var a=De(!0);return p("]"),ce(new ze({start:r,expression:n,property:a,end:o()}),e)}return e&&t("punc","(")?(i(),ce(new Me({start:r,expression:n,args:B(")"),end:o()}),!0)):n},le=function(n){var e=I.token;if(t("operator")&&zt(e.value)){i(),v();var r=H(je,e.value,le(n));return r.start=e,r.end=o(),r}for(var a=J(n);t("operator")&&Pt(I.token.value)&&!I.token.nlb;)a=H(Ie,I.token.value,a),a.start=e,a.end=I.token,i();return a},_e=function(n,e,r){var o=t("operator")?I.token.value:null;"in"==o&&r&&(o=null);var a=null!=o?It[o]:null;if(null!=a&&a>e){i();var u=_e(le(!0),a,r);return _e(new Ue({start:n.start,left:n,operator:o,right:u,end:u.end}),e,r)}return n},me=function(n){var e=I.token,r=z(n);if(t("operator","?")){i();var a=De(!1);return p(":"),new Ve({start:e,condition:r,consequent:a,alternative:De(!1,n),end:o()})}return r},be=function(n){var e=I.token,r=me(n),a=I.token.value;if(t("operator")&&jt(a)){if(P(r))return i(),new Le({start:e,left:r,operator:a,right:be(n),end:o()});u("Invalid assignment")}return r},De=function(n,e){var o=I.token,a=be(e);return n&&t("punc",",")?(i(),new Re({start:o,car:a,cdr:De(!0,e),end:r()})):a};return e.expression?De(!0):function(){for(var n=I.token,r=[];!t("eof");)r.push(U());var i=o(),a=e.toplevel;return a?(a.body=a.body.concat(r),a.end=i):a=new fe({start:n,body:r,end:i}),a}()}function z(n,e){E.call(this),this.before=n,this.after=e}function P(n,e,t){this.name=t.name,this.orig=[t],this.scope=n,this.references=[],this.global=!1,this.mangled_name=null,this.undeclared=!1,this.constant=!1,this.index=e}function j(n){function e(n,e){return n.replace(/[\u0080-\uffff]/g,function(n){var t=n.charCodeAt(0).toString(16);if(t.length<=2&&!e){for(;t.length<2;)t="0"+t;return"\\x"+t}for(;t.length<4;)t="0"+t;return"\\u"+t})}function t(t){var r=0,i=0;return t=t.replace(/[\\\b\f\n\r\t\x22\x27\u2028\u2029\0]/g,function(n){switch(n){case"\\":return"\\\\";case"\b":return"\\b";case"\f":return"\\f";case"\n":return"\\n";case"\r":return"\\r";case"\u2028":return"\\u2028";case"\u2029":return"\\u2029";case'"':return++r,'"';case"'":return++i,"'";case"\x00":return"\\x00"}return n}),n.ascii_only&&(t=e(t)),r>i?"'"+t.replace(/\x27/g,"\\'")+"'":'"'+t.replace(/\x22/g,'\\"')+'"'}function r(e){var r=t(e);return n.inline_script&&(r=r.replace(/<\x2fscript([>\/\t\n\f\r ])/gi,"<\\/script$1")),r}function i(t){return t=t.toString(),n.ascii_only&&(t=e(t,!0)),t}function o(e){return u(" ",n.indent_start+A-e*n.indent_level)}function a(){return k.charAt(k.length-1)}function s(){n.max_line_len&&w>n.max_line_len&&f("\n")}function f(e){e=String(e);var t=e.charAt(0);if(C&&(t&&!(";}".indexOf(t)<0)||/[;]$/.test(k)||(n.semicolons||x(t)?(F+=";",w++,D++):(F+="\n",D++,E++,w=0),n.beautify||(S=!1)),C=!1,s()),!n.beautify&&n.preserve_line&&q[q.length-1])for(var r=q[q.length-1].start.line;r>E;)F+="\n",D++,E++,w=0,S=!1;if(S){var i=a();(T(i)&&(T(t)||"\\"==t)||/^[\+\-\/]$/.test(t)&&t==i)&&(F+=" ",w++,D++),S=!1}var o=e.split(/\r?\n/),u=o.length-1;E+=u,0==u?w+=o[u].length:w=o[u].length,D+=e.length,k=e,F+=e}function p(){C=!1,f(";")}function d(){return A+n.indent_level}function h(n){var e;return f("{"),M(),O(d(),function(){e=n()}),$(),f("}"),e}function _(n){f("(");var e=n();return f(")"),e}function m(n){f("[");var e=n();return f("]"),e}function v(){f(","),B()}function b(){f(":"),n.space_colon&&B()}function y(){return F}n=c(n,{indent_start:0,indent_level:4,quote_keys:!1,space_colon:!0,ascii_only:!1,unescape_regexps:!1,inline_script:!1,width:80,max_line_len:32e3,beautify:!1,source_map:null,bracketize:!1,semicolons:!0,comments:!1,preserve_line:!1,screw_ie8:!1,preamble:null},!0);var A=0,w=0,E=1,D=0,F="",S=!1,C=!1,k=null,x=g("( [ + * / - , ."),B=n.beautify?function(){f(" ")}:function(){S=!0},$=n.beautify?function(e){n.beautify&&f(o(e?.5:0))}:l,O=n.beautify?function(n,e){n===!0&&(n=d());var t=A;A=n;var r=e();return A=t,r}:function(n,e){return e()},M=n.beautify?function(){f("\n")}:l,N=n.beautify?function(){f(";")}:function(){C=!0},R=n.source_map?function(e,t){try{e&&n.source_map.add(e.file||"?",E,w,e.line,e.col,t||"name"!=e.type?t:e.value)}catch(r){W.warn("Couldn't figure out mapping for {file}:{line},{col} → {cline},{ccol} [{name}]",{file:e.file,line:e.line,col:e.col,cline:E,ccol:w,name:t||""})}}:l;n.preamble&&f(n.preamble.replace(/\r\n?|[\n\u2028\u2029]|\s*$/g,"\n"));var q=[];return{get:y,toString:y,indent:$,indentation:function(){return A},current_width:function(){return w-A},should_break:function(){return n.width&&this.current_width()>=n.width},newline:M,print:f,space:B,comma:v,colon:b,last:function(){return k},semicolon:N,force_semicolon:p,to_ascii:e,print_name:function(n){f(i(n))},print_string:function(n){f(r(n))},next_indent:d,with_indent:O,with_block:h,with_parens:_,with_square:m,add_mapping:R,option:function(e){return n[e]},line:function(){return E},col:function(){return w},pos:function(){return D},push_node:function(n){q.push(n)},pop_node:function(){return q.pop()},stack:function(){return q},parent:function(n){return q[q.length-2-(n||0)]}}}function I(n,e){return this instanceof I?(z.call(this,this.before,this.after),void(this.options=c(n,{sequences:!e,properties:!e,dead_code:!e,drop_debugger:!e,unsafe:!1,unsafe_comps:!1,conditionals:!e,comparisons:!e,evaluate:!e,booleans:!e,loops:!e,unused:!e,hoist_funs:!e,keep_fargs:!1,hoist_vars:!1,if_return:!e,join_vars:!e,cascade:!e,side_effects:!e,pure_getters:!1,pure_funcs:null,negate_iife:!e,screw_ie8:!1,drop_console:!1,angular:!1,warnings:!0,global_defs:{}},!0))):new I(n,e)}function U(n){function e(e,i,o,a,u,s){if(r){var c=r.originalPositionFor({line:a,column:u});if(null===c.source)return;e=c.source,a=c.line,u=c.column,s=c.name}t.addMapping({generated:{line:i+n.dest_line_diff,column:o},original:{line:a+n.orig_line_diff,column:u},source:e,name:s})}n=c(n,{file:null,root:null,orig:null,orig_line_diff:0,dest_line_diff:0});var t=new MOZ_SourceMap.SourceMapGenerator({file:n.file,sourceRoot:n.root}),r=n.orig&&new MOZ_SourceMap.SourceMapConsumer(n.orig);return{add:e,get:function(){return t},toString:function(){return t.toString()}}}e.UglifyJS=n,s.prototype=Object.create(Error.prototype),s.prototype.constructor=s,s.croak=function(n,e){throw new s(n,e)};var V=function(){function n(n,o,a){function u(){var u=o(n[s],s),l=u instanceof r;return l&&(u=u.v),u instanceof e?(u=u.v,u instanceof t?f.push.apply(f,a?u.v.slice().reverse():u.v):f.push(u)):u!==i&&(u instanceof t?c.push.apply(c,a?u.v.slice().reverse():u.v):c.push(u)),l}var s,c=[],f=[];if(n instanceof Array)if(a){for(s=n.length;--s>=0&&!u(););c.reverse(),f.reverse()}else for(s=0;s SymbolDef for all variables/functions defined in this scope",functions:"[Object/S] like `variables`, but only lists function declarations",uses_with:"[boolean/S] tells whether this scope uses the `with` statement",uses_eval:"[boolean/S] tells whether this scope contains a direct call to the global `eval`",parent_scope:"[AST_Scope?/S] link to the parent scope",enclosed:"[SymbolDef*/S] a list of all symbol definitions that are accessed from this scope or any subscopes",cname:"[integer/S] current index for mangling variables (used internally by the mangler)"}},J),fe=A("Toplevel","globals",{$documentation:"The toplevel scope",$propdoc:{globals:"[Object/S] a map of name -> SymbolDef for all undeclared names"},wrap_enclose:function(n){var e=this,t=[],r=[];n.forEach(function(n){var e=n.lastIndexOf(":");t.push(n.substr(0,e)),r.push(n.substr(e+1))});var i="(function("+r.join(",")+"){ '$ORIG'; })("+t.join(",")+")";return i=H(i),i=i.transform(new z(function(n){return n instanceof G&&"$ORIG"==n.value?V.splice(e.body):void 0}))},wrap_commonjs:function(n,e){var t=this,r=[];e&&(t.figure_out_scope(),t.walk(new E(function(n){n instanceof nt&&n.definition().global&&(a(function(e){return e.name==n.name},r)||r.push(n))})));var i="(function(exports, global){ global['"+n+"'] = exports; '$ORIG'; '$EXPORTS'; }({}, (function(){return this}())))";return i=H(i),i=i.transform(new z(function(n){if(n instanceof K&&(n=n.body,n instanceof pt))switch(n.getValue()){case"$ORIG":return V.splice(t.body);case"$EXPORTS":var e=[];return r.forEach(function(n){e.push(new K({body:new Le({left:new ze({expression:new st({name:"exports"}),property:new pt({value:n.name})}),operator:"=",right:new st(n)})}))}),V.splice(e)}}))}},ce),le=A("Lambda","name argnames uses_arguments",{$documentation:"Base class for functions",$propdoc:{name:"[AST_SymbolDeclaration?] the name of this function",argnames:"[AST_SymbolFunarg*] array of function arguments",uses_arguments:"[boolean/S] tells whether this function accesses the arguments array"},_walk:function(n){return n._visit(this,function(){this.name&&this.name._walk(n),this.argnames.forEach(function(e){e._walk(n)}),w(this,n)})}},ce),pe=A("Accessor",null,{$documentation:"A setter/getter function. The `name` property is always null."},le),de=A("Function",null,{$documentation:"A function expression"},le),he=A("Defun",null,{$documentation:"A function definition"},le),_e=A("Jump",null,{$documentation:"Base class for “jumps” (for now that's `return`, `throw`, `break` and `continue`)"},Y),me=A("Exit","value",{$documentation:"Base class for “exits” (`return` and `throw`)",$propdoc:{value:"[AST_Node?] the value returned or thrown by this statement; could be null for AST_Return"},_walk:function(n){return n._visit(this,this.value&&function(){this.value._walk(n)})}},_e),ve=A("Return",null,{$documentation:"A `return` statement"},me),ge=A("Throw",null,{$documentation:"A `throw` statement"},me),be=A("LoopControl","label",{$documentation:"Base class for loop control statements (`break` and `continue`)",$propdoc:{label:"[AST_LabelRef?] the label, or null if none"},_walk:function(n){return n._visit(this,this.label&&function(){this.label._walk(n)})}},_e),ye=A("Break",null,{$documentation:"A `break` statement"},be),Ae=A("Continue",null,{$documentation:"A `continue` statement"},be),we=A("If","condition alternative",{$documentation:"A `if` statement",$propdoc:{condition:"[AST_Node] the `if` condition",alternative:"[AST_Statement?] the `else` part, or null if not present"},_walk:function(n){return n._visit(this,function(){this.condition._walk(n),this.body._walk(n),this.alternative&&this.alternative._walk(n)})}},ne),Ee=A("Switch","expression",{$documentation:"A `switch` statement",$propdoc:{expression:"[AST_Node] the `switch` “discriminant”"},_walk:function(n){return n._visit(this,function(){this.expression._walk(n),w(this,n)})}},J),De=A("SwitchBranch",null,{$documentation:"Base class for `switch` branches"},J),Fe=A("Default",null,{$documentation:"A `default` switch branch"},De),Se=A("Case","expression",{$documentation:"A `case` switch branch",$propdoc:{expression:"[AST_Node] the `case` expression"},_walk:function(n){return n._visit(this,function(){this.expression._walk(n),w(this,n)})}},De),Ce=A("Try","bcatch bfinally",{$documentation:"A `try` statement",$propdoc:{bcatch:"[AST_Catch?] the catch block, or null if not present",bfinally:"[AST_Finally?] the finally block, or null if not present"},_walk:function(n){return n._visit(this,function(){w(this,n),this.bcatch&&this.bcatch._walk(n),this.bfinally&&this.bfinally._walk(n)})}},J),ke=A("Catch","argname",{$documentation:"A `catch` node; only makes sense as part of a `try` statement",$propdoc:{argname:"[AST_SymbolCatch] symbol for the exception"},_walk:function(n){return n._visit(this,function(){this.argname._walk(n),w(this,n)
})}},J),xe=A("Finally",null,{$documentation:"A `finally` node; only makes sense as part of a `try` statement"},J),Be=A("Definitions","definitions",{$documentation:"Base class for `var` or `const` nodes (variable declarations/initializations)",$propdoc:{definitions:"[AST_VarDef*] array of variable definitions"},_walk:function(n){return n._visit(this,function(){this.definitions.forEach(function(e){e._walk(n)})})}},Y),Te=A("Var",null,{$documentation:"A `var` statement"},Be),$e=A("Const",null,{$documentation:"A `const` statement"},Be),Oe=A("VarDef","name value",{$documentation:"A variable declaration; only appears in a AST_Definitions node",$propdoc:{name:"[AST_SymbolVar|AST_SymbolConst] name of the variable",value:"[AST_Node?] initializer, or null of there's no initializer"},_walk:function(n){return n._visit(this,function(){this.name._walk(n),this.value&&this.value._walk(n)})}}),Me=A("Call","expression args",{$documentation:"A function call expression",$propdoc:{expression:"[AST_Node] expression to invoke as function",args:"[AST_Node*] array of arguments"},_walk:function(n){return n._visit(this,function(){this.expression._walk(n),this.args.forEach(function(e){e._walk(n)})})}}),Ne=A("New",null,{$documentation:"An object instantiation. Derives from a function call since it has exactly the same properties"},Me),Re=A("Seq","car cdr",{$documentation:"A sequence expression (two comma-separated expressions)",$propdoc:{car:"[AST_Node] first element in sequence",cdr:"[AST_Node] second element in sequence"},$cons:function(n,e){var t=new Re(n);return t.car=n,t.cdr=e,t},$from_array:function(n){if(0==n.length)return null;if(1==n.length)return n[0].clone();for(var e=null,t=n.length;--t>=0;)e=Re.cons(n[t],e);for(var r=e;r;){if(r.cdr&&!r.cdr.cdr){r.cdr=r.cdr.car;break}r=r.cdr}return e},to_array:function(){for(var n=this,e=[];n;){if(e.push(n.car),n.cdr&&!(n.cdr instanceof Re)){e.push(n.cdr);break}n=n.cdr}return e},add:function(n){for(var e=this;e;){if(!(e.cdr instanceof Re)){var t=Re.cons(e.cdr,n);return e.cdr=t}e=e.cdr}},_walk:function(n){return n._visit(this,function(){this.car._walk(n),this.cdr&&this.cdr._walk(n)})}}),qe=A("PropAccess","expression property",{$documentation:'Base class for property access expressions, i.e. `a.foo` or `a["foo"]`',$propdoc:{expression:"[AST_Node] the “container” expression",property:"[AST_Node|string] the property to access. For AST_Dot this is always a plain string, while for AST_Sub it's an arbitrary AST_Node"}}),He=A("Dot",null,{$documentation:"A dotted property access expression",_walk:function(n){return n._visit(this,function(){this.expression._walk(n)})}},qe),ze=A("Sub",null,{$documentation:'Index-style property access, i.e. `a["foo"]`',_walk:function(n){return n._visit(this,function(){this.expression._walk(n),this.property._walk(n)})}},qe),Pe=A("Unary","operator expression",{$documentation:"Base class for unary expressions",$propdoc:{operator:"[string] the operator",expression:"[AST_Node] expression that this unary operator applies to"},_walk:function(n){return n._visit(this,function(){this.expression._walk(n)})}}),je=A("UnaryPrefix",null,{$documentation:"Unary prefix expression, i.e. `typeof i` or `++i`"},Pe),Ie=A("UnaryPostfix",null,{$documentation:"Unary postfix expression, i.e. `i++`"},Pe),Ue=A("Binary","left operator right",{$documentation:"Binary expression, i.e. `a + b`",$propdoc:{left:"[AST_Node] left-hand side expression",operator:"[string] the operator",right:"[AST_Node] right-hand side expression"},_walk:function(n){return n._visit(this,function(){this.left._walk(n),this.right._walk(n)})}}),Ve=A("Conditional","condition consequent alternative",{$documentation:"Conditional expression using the ternary operator, i.e. `a ? b : c`",$propdoc:{condition:"[AST_Node]",consequent:"[AST_Node]",alternative:"[AST_Node]"},_walk:function(n){return n._visit(this,function(){this.condition._walk(n),this.consequent._walk(n),this.alternative._walk(n)})}}),Le=A("Assign",null,{$documentation:"An assignment expression — `a = b + 5`"},Ue),We=A("Array","elements",{$documentation:"An array literal",$propdoc:{elements:"[AST_Node*] array of elements"},_walk:function(n){return n._visit(this,function(){this.elements.forEach(function(e){e._walk(n)})})}}),Ye=A("Object","properties",{$documentation:"An object literal",$propdoc:{properties:"[AST_ObjectProperty*] array of properties"},_walk:function(n){return n._visit(this,function(){this.properties.forEach(function(e){e._walk(n)})})}}),Xe=A("ObjectProperty","key value",{$documentation:"Base class for literal object properties",$propdoc:{key:"[string] the property name converted to a string for ObjectKeyVal. For setters and getters this is an arbitrary AST_Node.",value:"[AST_Node] property value. For setters and getters this is an AST_Function."},_walk:function(n){return n._visit(this,function(){this.value._walk(n)})}}),Ge=A("ObjectKeyVal",null,{$documentation:"A key: value object property"},Xe),Ke=A("ObjectSetter",null,{$documentation:"An object setter property"},Xe),Je=A("ObjectGetter",null,{$documentation:"An object getter property"},Xe),Ze=A("Symbol","scope name thedef",{$propdoc:{name:"[string] name of this symbol",scope:"[AST_Scope/S] the current scope (not necessarily the definition scope)",thedef:"[SymbolDef/S] the definition of this symbol"},$documentation:"Base class for all symbols"}),Qe=A("SymbolAccessor",null,{$documentation:"The name of a property accessor (setter/getter function)"},Ze),nt=A("SymbolDeclaration","init",{$documentation:"A declaration symbol (symbol in var/const, function name or argument, symbol in catch)",$propdoc:{init:"[AST_Node*/S] array of initializers for this declaration."}},Ze),et=A("SymbolVar",null,{$documentation:"Symbol defining a variable"},nt),tt=A("SymbolConst",null,{$documentation:"A constant declaration"},nt),rt=A("SymbolFunarg",null,{$documentation:"Symbol naming a function argument"},et),it=A("SymbolDefun",null,{$documentation:"Symbol defining a function"},nt),ot=A("SymbolLambda",null,{$documentation:"Symbol naming a function expression"},nt),at=A("SymbolCatch",null,{$documentation:"Symbol naming the exception in catch"},nt),ut=A("Label","references",{$documentation:"Symbol naming a label (declaration)",$propdoc:{references:"[AST_LoopControl*] a list of nodes referring to this label"},initialize:function(){this.references=[],this.thedef=this}},Ze),st=A("SymbolRef",null,{$documentation:"Reference to some symbol (not definition/declaration)"},Ze),ct=A("LabelRef",null,{$documentation:"Reference to a label symbol"},Ze),ft=A("This",null,{$documentation:"The `this` symbol"},Ze),lt=A("Constant",null,{$documentation:"Base class for all constants",getValue:function(){return this.value}}),pt=A("String","value",{$documentation:"A string literal",$propdoc:{value:"[string] the contents of this string"}},lt),dt=A("Number","value",{$documentation:"A number literal",$propdoc:{value:"[number] the numeric value"}},lt),ht=A("RegExp","value",{$documentation:"A regexp literal",$propdoc:{value:"[RegExp] the actual regexp"}},lt),_t=A("Atom",null,{$documentation:"Base class for atoms"},lt),mt=A("Null",null,{$documentation:"The `null` atom",value:null},_t),vt=A("NaN",null,{$documentation:"The impossible value",value:0/0},_t),gt=A("Undefined",null,{$documentation:"The `undefined` value",value:void 0},_t),bt=A("Hole",null,{$documentation:"A hole in an array",value:void 0},_t),yt=A("Infinity",null,{$documentation:"The `Infinity` value",value:1/0},_t),At=A("Boolean",null,{$documentation:"Base class for booleans"},_t),wt=A("False",null,{$documentation:"The `false` atom",value:!1},At),Et=A("True",null,{$documentation:"The `true` atom",value:!0},At);E.prototype={_visit:function(n,e){this.stack.push(n);var t=this.visit(n,e?function(){e.call(n)}:l);return!t&&e&&e.call(n),this.stack.pop(),t},parent:function(n){return this.stack[this.stack.length-2-(n||0)]},push:function(n){this.stack.push(n)},pop:function(){return this.stack.pop()},self:function(){return this.stack[this.stack.length-1]},find_parent:function(n){for(var e=this.stack,t=e.length;--t>=0;){var r=e[t];if(r instanceof n)return r}},has_directive:function(n){return this.find_parent(ce).has_directive(n)},in_boolean_context:function(){for(var n=this.stack,e=n.length,t=n[--e];e>0;){var r=n[--e];if(r instanceof we&&r.condition===t||r instanceof Ve&&r.condition===t||r instanceof re&&r.condition===t||r instanceof ae&&r.condition===t||r instanceof je&&"!"==r.operator&&r.expression===t)return!0;if(!(r instanceof Ue)||"&&"!=r.operator&&"||"!=r.operator)return!1;t=r}},loopcontrol_target:function(n){var e=this.stack;if(n)for(var t=e.length;--t>=0;){var r=e[t];if(r instanceof ee&&r.label.name==n.name)return r.body}else for(var t=e.length;--t>=0;){var r=e[t];if(r instanceof Ee||r instanceof te)return r}}};var Dt="break case catch const continue debugger default delete do else finally for function if in instanceof new return switch throw try typeof var void while with",Ft="false null true",St="abstract boolean byte char class double enum export extends final float goto implements import int interface long native package private protected public short static super synchronized this throws transient volatile yield "+Ft+" "+Dt,Ct="return new delete throw else case";Dt=g(Dt),St=g(St),Ct=g(Ct),Ft=g(Ft);var kt=g(i("+-*&%=<>!?|~^")),xt=/^0x[0-9a-f]+$/i,Bt=/^0[0-7]+$/,Tt=/^\d*\.?\d*(?:e[+-]?\d*(?:\d\.?|\.?\d)\d*)?$/i,$t=g(["in","instanceof","typeof","new","void","delete","++","--","+","-","!","~","&","|","^","*","/","%",">>","<<",">>>","<",">","<=",">=","==","===","!=","!==","?","=","+=","-=","/=","*=","%=",">>=","<<=",">>>=","|=","^=","&=","&&","||"]),Ot=g(i(" \n\r \f ")),Mt=g(i("[{(,.;:")),Nt=g(i("[]{}(),;:")),Rt=g(i("gmsiy")),qt={letter:new RegExp("[\\u0041-\\u005A\\u0061-\\u007A\\u00AA\\u00B5\\u00BA\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02C1\\u02C6-\\u02D1\\u02E0-\\u02E4\\u02EC\\u02EE\\u0370-\\u0374\\u0376\\u0377\\u037A-\\u037D\\u0386\\u0388-\\u038A\\u038C\\u038E-\\u03A1\\u03A3-\\u03F5\\u03F7-\\u0481\\u048A-\\u0523\\u0531-\\u0556\\u0559\\u0561-\\u0587\\u05D0-\\u05EA\\u05F0-\\u05F2\\u0621-\\u064A\\u066E\\u066F\\u0671-\\u06D3\\u06D5\\u06E5\\u06E6\\u06EE\\u06EF\\u06FA-\\u06FC\\u06FF\\u0710\\u0712-\\u072F\\u074D-\\u07A5\\u07B1\\u07CA-\\u07EA\\u07F4\\u07F5\\u07FA\\u0904-\\u0939\\u093D\\u0950\\u0958-\\u0961\\u0971\\u0972\\u097B-\\u097F\\u0985-\\u098C\\u098F\\u0990\\u0993-\\u09A8\\u09AA-\\u09B0\\u09B2\\u09B6-\\u09B9\\u09BD\\u09CE\\u09DC\\u09DD\\u09DF-\\u09E1\\u09F0\\u09F1\\u0A05-\\u0A0A\\u0A0F\\u0A10\\u0A13-\\u0A28\\u0A2A-\\u0A30\\u0A32\\u0A33\\u0A35\\u0A36\\u0A38\\u0A39\\u0A59-\\u0A5C\\u0A5E\\u0A72-\\u0A74\\u0A85-\\u0A8D\\u0A8F-\\u0A91\\u0A93-\\u0AA8\\u0AAA-\\u0AB0\\u0AB2\\u0AB3\\u0AB5-\\u0AB9\\u0ABD\\u0AD0\\u0AE0\\u0AE1\\u0B05-\\u0B0C\\u0B0F\\u0B10\\u0B13-\\u0B28\\u0B2A-\\u0B30\\u0B32\\u0B33\\u0B35-\\u0B39\\u0B3D\\u0B5C\\u0B5D\\u0B5F-\\u0B61\\u0B71\\u0B83\\u0B85-\\u0B8A\\u0B8E-\\u0B90\\u0B92-\\u0B95\\u0B99\\u0B9A\\u0B9C\\u0B9E\\u0B9F\\u0BA3\\u0BA4\\u0BA8-\\u0BAA\\u0BAE-\\u0BB9\\u0BD0\\u0C05-\\u0C0C\\u0C0E-\\u0C10\\u0C12-\\u0C28\\u0C2A-\\u0C33\\u0C35-\\u0C39\\u0C3D\\u0C58\\u0C59\\u0C60\\u0C61\\u0C85-\\u0C8C\\u0C8E-\\u0C90\\u0C92-\\u0CA8\\u0CAA-\\u0CB3\\u0CB5-\\u0CB9\\u0CBD\\u0CDE\\u0CE0\\u0CE1\\u0D05-\\u0D0C\\u0D0E-\\u0D10\\u0D12-\\u0D28\\u0D2A-\\u0D39\\u0D3D\\u0D60\\u0D61\\u0D7A-\\u0D7F\\u0D85-\\u0D96\\u0D9A-\\u0DB1\\u0DB3-\\u0DBB\\u0DBD\\u0DC0-\\u0DC6\\u0E01-\\u0E30\\u0E32\\u0E33\\u0E40-\\u0E46\\u0E81\\u0E82\\u0E84\\u0E87\\u0E88\\u0E8A\\u0E8D\\u0E94-\\u0E97\\u0E99-\\u0E9F\\u0EA1-\\u0EA3\\u0EA5\\u0EA7\\u0EAA\\u0EAB\\u0EAD-\\u0EB0\\u0EB2\\u0EB3\\u0EBD\\u0EC0-\\u0EC4\\u0EC6\\u0EDC\\u0EDD\\u0F00\\u0F40-\\u0F47\\u0F49-\\u0F6C\\u0F88-\\u0F8B\\u1000-\\u102A\\u103F\\u1050-\\u1055\\u105A-\\u105D\\u1061\\u1065\\u1066\\u106E-\\u1070\\u1075-\\u1081\\u108E\\u10A0-\\u10C5\\u10D0-\\u10FA\\u10FC\\u1100-\\u1159\\u115F-\\u11A2\\u11A8-\\u11F9\\u1200-\\u1248\\u124A-\\u124D\\u1250-\\u1256\\u1258\\u125A-\\u125D\\u1260-\\u1288\\u128A-\\u128D\\u1290-\\u12B0\\u12B2-\\u12B5\\u12B8-\\u12BE\\u12C0\\u12C2-\\u12C5\\u12C8-\\u12D6\\u12D8-\\u1310\\u1312-\\u1315\\u1318-\\u135A\\u1380-\\u138F\\u13A0-\\u13F4\\u1401-\\u166C\\u166F-\\u1676\\u1681-\\u169A\\u16A0-\\u16EA\\u1700-\\u170C\\u170E-\\u1711\\u1720-\\u1731\\u1740-\\u1751\\u1760-\\u176C\\u176E-\\u1770\\u1780-\\u17B3\\u17D7\\u17DC\\u1820-\\u1877\\u1880-\\u18A8\\u18AA\\u1900-\\u191C\\u1950-\\u196D\\u1970-\\u1974\\u1980-\\u19A9\\u19C1-\\u19C7\\u1A00-\\u1A16\\u1B05-\\u1B33\\u1B45-\\u1B4B\\u1B83-\\u1BA0\\u1BAE\\u1BAF\\u1C00-\\u1C23\\u1C4D-\\u1C4F\\u1C5A-\\u1C7D\\u1D00-\\u1DBF\\u1E00-\\u1F15\\u1F18-\\u1F1D\\u1F20-\\u1F45\\u1F48-\\u1F4D\\u1F50-\\u1F57\\u1F59\\u1F5B\\u1F5D\\u1F5F-\\u1F7D\\u1F80-\\u1FB4\\u1FB6-\\u1FBC\\u1FBE\\u1FC2-\\u1FC4\\u1FC6-\\u1FCC\\u1FD0-\\u1FD3\\u1FD6-\\u1FDB\\u1FE0-\\u1FEC\\u1FF2-\\u1FF4\\u1FF6-\\u1FFC\\u2071\\u207F\\u2090-\\u2094\\u2102\\u2107\\u210A-\\u2113\\u2115\\u2119-\\u211D\\u2124\\u2126\\u2128\\u212A-\\u212D\\u212F-\\u2139\\u213C-\\u213F\\u2145-\\u2149\\u214E\\u2183\\u2184\\u2C00-\\u2C2E\\u2C30-\\u2C5E\\u2C60-\\u2C6F\\u2C71-\\u2C7D\\u2C80-\\u2CE4\\u2D00-\\u2D25\\u2D30-\\u2D65\\u2D6F\\u2D80-\\u2D96\\u2DA0-\\u2DA6\\u2DA8-\\u2DAE\\u2DB0-\\u2DB6\\u2DB8-\\u2DBE\\u2DC0-\\u2DC6\\u2DC8-\\u2DCE\\u2DD0-\\u2DD6\\u2DD8-\\u2DDE\\u2E2F\\u3005\\u3006\\u3031-\\u3035\\u303B\\u303C\\u3041-\\u3096\\u309D-\\u309F\\u30A1-\\u30FA\\u30FC-\\u30FF\\u3105-\\u312D\\u3131-\\u318E\\u31A0-\\u31B7\\u31F0-\\u31FF\\u3400\\u4DB5\\u4E00\\u9FC3\\uA000-\\uA48C\\uA500-\\uA60C\\uA610-\\uA61F\\uA62A\\uA62B\\uA640-\\uA65F\\uA662-\\uA66E\\uA67F-\\uA697\\uA717-\\uA71F\\uA722-\\uA788\\uA78B\\uA78C\\uA7FB-\\uA801\\uA803-\\uA805\\uA807-\\uA80A\\uA80C-\\uA822\\uA840-\\uA873\\uA882-\\uA8B3\\uA90A-\\uA925\\uA930-\\uA946\\uAA00-\\uAA28\\uAA40-\\uAA42\\uAA44-\\uAA4B\\uAC00\\uD7A3\\uF900-\\uFA2D\\uFA30-\\uFA6A\\uFA70-\\uFAD9\\uFB00-\\uFB06\\uFB13-\\uFB17\\uFB1D\\uFB1F-\\uFB28\\uFB2A-\\uFB36\\uFB38-\\uFB3C\\uFB3E\\uFB40\\uFB41\\uFB43\\uFB44\\uFB46-\\uFBB1\\uFBD3-\\uFD3D\\uFD50-\\uFD8F\\uFD92-\\uFDC7\\uFDF0-\\uFDFB\\uFE70-\\uFE74\\uFE76-\\uFEFC\\uFF21-\\uFF3A\\uFF41-\\uFF5A\\uFF66-\\uFFBE\\uFFC2-\\uFFC7\\uFFCA-\\uFFCF\\uFFD2-\\uFFD7\\uFFDA-\\uFFDC]"),non_spacing_mark:new RegExp("[\\u0300-\\u036F\\u0483-\\u0487\\u0591-\\u05BD\\u05BF\\u05C1\\u05C2\\u05C4\\u05C5\\u05C7\\u0610-\\u061A\\u064B-\\u065E\\u0670\\u06D6-\\u06DC\\u06DF-\\u06E4\\u06E7\\u06E8\\u06EA-\\u06ED\\u0711\\u0730-\\u074A\\u07A6-\\u07B0\\u07EB-\\u07F3\\u0816-\\u0819\\u081B-\\u0823\\u0825-\\u0827\\u0829-\\u082D\\u0900-\\u0902\\u093C\\u0941-\\u0948\\u094D\\u0951-\\u0955\\u0962\\u0963\\u0981\\u09BC\\u09C1-\\u09C4\\u09CD\\u09E2\\u09E3\\u0A01\\u0A02\\u0A3C\\u0A41\\u0A42\\u0A47\\u0A48\\u0A4B-\\u0A4D\\u0A51\\u0A70\\u0A71\\u0A75\\u0A81\\u0A82\\u0ABC\\u0AC1-\\u0AC5\\u0AC7\\u0AC8\\u0ACD\\u0AE2\\u0AE3\\u0B01\\u0B3C\\u0B3F\\u0B41-\\u0B44\\u0B4D\\u0B56\\u0B62\\u0B63\\u0B82\\u0BC0\\u0BCD\\u0C3E-\\u0C40\\u0C46-\\u0C48\\u0C4A-\\u0C4D\\u0C55\\u0C56\\u0C62\\u0C63\\u0CBC\\u0CBF\\u0CC6\\u0CCC\\u0CCD\\u0CE2\\u0CE3\\u0D41-\\u0D44\\u0D4D\\u0D62\\u0D63\\u0DCA\\u0DD2-\\u0DD4\\u0DD6\\u0E31\\u0E34-\\u0E3A\\u0E47-\\u0E4E\\u0EB1\\u0EB4-\\u0EB9\\u0EBB\\u0EBC\\u0EC8-\\u0ECD\\u0F18\\u0F19\\u0F35\\u0F37\\u0F39\\u0F71-\\u0F7E\\u0F80-\\u0F84\\u0F86\\u0F87\\u0F90-\\u0F97\\u0F99-\\u0FBC\\u0FC6\\u102D-\\u1030\\u1032-\\u1037\\u1039\\u103A\\u103D\\u103E\\u1058\\u1059\\u105E-\\u1060\\u1071-\\u1074\\u1082\\u1085\\u1086\\u108D\\u109D\\u135F\\u1712-\\u1714\\u1732-\\u1734\\u1752\\u1753\\u1772\\u1773\\u17B7-\\u17BD\\u17C6\\u17C9-\\u17D3\\u17DD\\u180B-\\u180D\\u18A9\\u1920-\\u1922\\u1927\\u1928\\u1932\\u1939-\\u193B\\u1A17\\u1A18\\u1A56\\u1A58-\\u1A5E\\u1A60\\u1A62\\u1A65-\\u1A6C\\u1A73-\\u1A7C\\u1A7F\\u1B00-\\u1B03\\u1B34\\u1B36-\\u1B3A\\u1B3C\\u1B42\\u1B6B-\\u1B73\\u1B80\\u1B81\\u1BA2-\\u1BA5\\u1BA8\\u1BA9\\u1C2C-\\u1C33\\u1C36\\u1C37\\u1CD0-\\u1CD2\\u1CD4-\\u1CE0\\u1CE2-\\u1CE8\\u1CED\\u1DC0-\\u1DE6\\u1DFD-\\u1DFF\\u20D0-\\u20DC\\u20E1\\u20E5-\\u20F0\\u2CEF-\\u2CF1\\u2DE0-\\u2DFF\\u302A-\\u302F\\u3099\\u309A\\uA66F\\uA67C\\uA67D\\uA6F0\\uA6F1\\uA802\\uA806\\uA80B\\uA825\\uA826\\uA8C4\\uA8E0-\\uA8F1\\uA926-\\uA92D\\uA947-\\uA951\\uA980-\\uA982\\uA9B3\\uA9B6-\\uA9B9\\uA9BC\\uAA29-\\uAA2E\\uAA31\\uAA32\\uAA35\\uAA36\\uAA43\\uAA4C\\uAAB0\\uAAB2-\\uAAB4\\uAAB7\\uAAB8\\uAABE\\uAABF\\uAAC1\\uABE5\\uABE8\\uABED\\uFB1E\\uFE00-\\uFE0F\\uFE20-\\uFE26]"),space_combining_mark:new RegExp("[\\u0903\\u093E-\\u0940\\u0949-\\u094C\\u094E\\u0982\\u0983\\u09BE-\\u09C0\\u09C7\\u09C8\\u09CB\\u09CC\\u09D7\\u0A03\\u0A3E-\\u0A40\\u0A83\\u0ABE-\\u0AC0\\u0AC9\\u0ACB\\u0ACC\\u0B02\\u0B03\\u0B3E\\u0B40\\u0B47\\u0B48\\u0B4B\\u0B4C\\u0B57\\u0BBE\\u0BBF\\u0BC1\\u0BC2\\u0BC6-\\u0BC8\\u0BCA-\\u0BCC\\u0BD7\\u0C01-\\u0C03\\u0C41-\\u0C44\\u0C82\\u0C83\\u0CBE\\u0CC0-\\u0CC4\\u0CC7\\u0CC8\\u0CCA\\u0CCB\\u0CD5\\u0CD6\\u0D02\\u0D03\\u0D3E-\\u0D40\\u0D46-\\u0D48\\u0D4A-\\u0D4C\\u0D57\\u0D82\\u0D83\\u0DCF-\\u0DD1\\u0DD8-\\u0DDF\\u0DF2\\u0DF3\\u0F3E\\u0F3F\\u0F7F\\u102B\\u102C\\u1031\\u1038\\u103B\\u103C\\u1056\\u1057\\u1062-\\u1064\\u1067-\\u106D\\u1083\\u1084\\u1087-\\u108C\\u108F\\u109A-\\u109C\\u17B6\\u17BE-\\u17C5\\u17C7\\u17C8\\u1923-\\u1926\\u1929-\\u192B\\u1930\\u1931\\u1933-\\u1938\\u19B0-\\u19C0\\u19C8\\u19C9\\u1A19-\\u1A1B\\u1A55\\u1A57\\u1A61\\u1A63\\u1A64\\u1A6D-\\u1A72\\u1B04\\u1B35\\u1B3B\\u1B3D-\\u1B41\\u1B43\\u1B44\\u1B82\\u1BA1\\u1BA6\\u1BA7\\u1BAA\\u1C24-\\u1C2B\\u1C34\\u1C35\\u1CE1\\u1CF2\\uA823\\uA824\\uA827\\uA880\\uA881\\uA8B4-\\uA8C3\\uA952\\uA953\\uA983\\uA9B4\\uA9B5\\uA9BA\\uA9BB\\uA9BD-\\uA9C0\\uAA2F\\uAA30\\uAA33\\uAA34\\uAA4D\\uAA7B\\uABE3\\uABE4\\uABE6\\uABE7\\uABE9\\uABEA\\uABEC]"),connector_punctuation:new RegExp("[\\u005F\\u203F\\u2040\\u2054\\uFE33\\uFE34\\uFE4D-\\uFE4F\\uFF3F]")};M.prototype.toString=function(){return this.message+" (line: "+this.line+", col: "+this.col+", pos: "+this.pos+")\n\n"+this.stack};var Ht={},zt=g(["typeof","void","delete","--","++","!","~","-","+"]),Pt=g(["--","++"]),jt=g(["=","+=","-=","/=","*=","%=",">>=","<<=",">>>=","|=","^=","&="]),It=function(n,e){for(var t=0;t","<=",">=","in","instanceof"],[">>","<<",">>>"],["+","-"],["*","/","%"]],{}),Ut=t(["for","do","while","switch"]),Vt=t(["atom","num","string","regexp","name"]);z.prototype=new E,function(n){function e(e,t){e.DEFMETHOD("transform",function(e,r){var i,o;return e.push(this),e.before&&(i=e.before(this,t,r)),i===n&&(e.after?(e.stack[e.stack.length-1]=i=this.clone(),t(i,e),o=e.after(i,r),o!==n&&(i=o)):(i=this,t(i,e))),e.pop(),i})}function t(n,e){return V(n,function(n){return n.transform(e,!0)})}e(W,l),e(ee,function(n,e){n.label=n.label.transform(e),n.body=n.body.transform(e)}),e(K,function(n,e){n.body=n.body.transform(e)}),e(J,function(n,e){n.body=t(n.body,e)}),e(re,function(n,e){n.condition=n.condition.transform(e),n.body=n.body.transform(e)}),e(ae,function(n,e){n.init&&(n.init=n.init.transform(e)),n.condition&&(n.condition=n.condition.transform(e)),n.step&&(n.step=n.step.transform(e)),n.body=n.body.transform(e)}),e(ue,function(n,e){n.init=n.init.transform(e),n.object=n.object.transform(e),n.body=n.body.transform(e)}),e(se,function(n,e){n.expression=n.expression.transform(e),n.body=n.body.transform(e)}),e(me,function(n,e){n.value&&(n.value=n.value.transform(e))}),e(be,function(n,e){n.label&&(n.label=n.label.transform(e))}),e(we,function(n,e){n.condition=n.condition.transform(e),n.body=n.body.transform(e),n.alternative&&(n.alternative=n.alternative.transform(e))}),e(Ee,function(n,e){n.expression=n.expression.transform(e),n.body=t(n.body,e)}),e(Se,function(n,e){n.expression=n.expression.transform(e),n.body=t(n.body,e)}),e(Ce,function(n,e){n.body=t(n.body,e),n.bcatch&&(n.bcatch=n.bcatch.transform(e)),n.bfinally&&(n.bfinally=n.bfinally.transform(e))}),e(ke,function(n,e){n.argname=n.argname.transform(e),n.body=t(n.body,e)}),e(Be,function(n,e){n.definitions=t(n.definitions,e)}),e(Oe,function(n,e){n.name=n.name.transform(e),n.value&&(n.value=n.value.transform(e))}),e(le,function(n,e){n.name&&(n.name=n.name.transform(e)),n.argnames=t(n.argnames,e),n.body=t(n.body,e)}),e(Me,function(n,e){n.expression=n.expression.transform(e),n.args=t(n.args,e)}),e(Re,function(n,e){n.car=n.car.transform(e),n.cdr=n.cdr.transform(e)}),e(He,function(n,e){n.expression=n.expression.transform(e)}),e(ze,function(n,e){n.expression=n.expression.transform(e),n.property=n.property.transform(e)}),e(Pe,function(n,e){n.expression=n.expression.transform(e)}),e(Ue,function(n,e){n.left=n.left.transform(e),n.right=n.right.transform(e)}),e(Ve,function(n,e){n.condition=n.condition.transform(e),n.consequent=n.consequent.transform(e),n.alternative=n.alternative.transform(e)}),e(We,function(n,e){n.elements=t(n.elements,e)}),e(Ye,function(n,e){n.properties=t(n.properties,e)}),e(Xe,function(n,e){n.value=n.value.transform(e)})}(),P.prototype={unmangleable:function(n){return this.global&&!(n&&n.toplevel)||this.undeclared||!(n&&n.eval)&&(this.scope.uses_eval||this.scope.uses_with)},mangle:function(n){if(!this.mangled_name&&!this.unmangleable(n)){var e=this.scope;!n.screw_ie8&&this.orig[0]instanceof ot&&(e=e.parent_scope),this.mangled_name=e.next_mangled(n,this)}}},fe.DEFMETHOD("figure_out_scope",function(n){n=c(n,{screw_ie8:!1});var e=this,t=e.parent_scope=null,r=null,i=0,o=new E(function(e,a){if(n.screw_ie8&&e instanceof ke){var u=t;return t=new ce(e),t.init_scope_vars(i),t.parent_scope=u,a(),t=u,!0}if(e instanceof ce){e.init_scope_vars(i);var u=e.parent_scope=t,s=r;return r=t=e,++i,a(),--i,t=u,r=s,!0}if(e instanceof G)return e.scope=t,p(t.directives,e.value),!0;if(e instanceof se)for(var c=t;c;c=c.parent_scope)c.uses_with=!0;else if(e instanceof Ze&&(e.scope=t),e instanceof ot)r.def_function(e);else if(e instanceof it)(e.scope=r.parent_scope).def_function(e);else if(e instanceof et||e instanceof tt){var f=r.def_variable(e);f.constant=e instanceof tt,f.init=o.parent().value}else e instanceof at&&(n.screw_ie8?t:r).def_variable(e)});e.walk(o);var a=null,u=e.globals=new y,o=new E(function(n,t){if(n instanceof le){var r=a;return a=n,t(),a=r,!0}if(n instanceof st){var i=n.name,s=n.scope.find_variable(i);if(s)n.thedef=s;else{var c;if(u.has(i)?c=u.get(i):(c=new P(e,u.size(),n),c.undeclared=!0,c.global=!0,u.set(i,c)),n.thedef=c,"eval"==i&&o.parent()instanceof Me)for(var f=n.scope;f&&!f.uses_eval;f=f.parent_scope)f.uses_eval=!0;a&&"arguments"==i&&(a.uses_arguments=!0)}return n.reference(),!0}});e.walk(o)}),ce.DEFMETHOD("init_scope_vars",function(n){this.directives=[],this.variables=new y,this.functions=new y,this.uses_with=!1,this.uses_eval=!1,this.parent_scope=null,this.enclosed=[],this.cname=-1,this.nesting=n}),ce.DEFMETHOD("strict",function(){return this.has_directive("use strict")}),le.DEFMETHOD("init_scope_vars",function(){ce.prototype.init_scope_vars.apply(this,arguments),this.uses_arguments=!1}),st.DEFMETHOD("reference",function(){var n=this.definition();n.references.push(this);for(var e=this.scope;e&&(p(e.enclosed,n),e!==n.scope);)e=e.parent_scope;this.frame=this.scope.nesting-n.scope.nesting}),ce.DEFMETHOD("find_variable",function(n){return n instanceof Ze&&(n=n.name),this.variables.get(n)||this.parent_scope&&this.parent_scope.find_variable(n)}),ce.DEFMETHOD("has_directive",function(n){return this.parent_scope&&this.parent_scope.has_directive(n)||(this.directives.indexOf(n)>=0?this:null)}),ce.DEFMETHOD("def_function",function(n){this.functions.set(n.name,this.def_variable(n))}),ce.DEFMETHOD("def_variable",function(n){var e;return this.variables.has(n.name)?(e=this.variables.get(n.name),e.orig.push(n)):(e=new P(this,this.variables.size(),n),this.variables.set(n.name,e),e.global=!this.parent_scope),n.thedef=e}),ce.DEFMETHOD("next_mangled",function(n){var e=this.enclosed;n:for(;;){var t=Lt(++this.cname);if(x(t)&&!(n.except.indexOf(t)>=0)){for(var r=e.length;--r>=0;){var i=e[r],o=i.mangled_name||i.unmangleable(n)&&i.name;if(t==o)continue n}return t}}}),de.DEFMETHOD("next_mangled",function(n,e){for(var t=e.orig[0]instanceof rt&&this.name&&this.name.definition();;){var r=le.prototype.next_mangled.call(this,n,e);if(!t||t.mangled_name!=r)return r}}),ce.DEFMETHOD("references",function(n){return n instanceof Ze&&(n=n.definition()),this.enclosed.indexOf(n)<0?null:n}),Ze.DEFMETHOD("unmangleable",function(n){return this.definition().unmangleable(n)}),Qe.DEFMETHOD("unmangleable",function(){return!0}),ut.DEFMETHOD("unmangleable",function(){return!1}),Ze.DEFMETHOD("unreferenced",function(){return 0==this.definition().references.length&&!(this.scope.uses_eval||this.scope.uses_with)}),Ze.DEFMETHOD("undeclared",function(){return this.definition().undeclared}),ct.DEFMETHOD("undeclared",function(){return!1}),ut.DEFMETHOD("undeclared",function(){return!1}),Ze.DEFMETHOD("definition",function(){return this.thedef}),Ze.DEFMETHOD("global",function(){return this.definition().global}),fe.DEFMETHOD("_default_mangler_options",function(n){return c(n,{except:[],eval:!1,sort:!1,toplevel:!1,screw_ie8:!1})}),fe.DEFMETHOD("mangle_names",function(n){n=this._default_mangler_options(n);var e=-1,t=[],r=new E(function(i,o){if(i instanceof ee){var a=e;return o(),e=a,!0}if(i instanceof ce){var u=(r.parent(),[]);return i.variables.each(function(e){n.except.indexOf(e.name)<0&&u.push(e)}),n.sort&&u.sort(function(n,e){return e.references.length-n.references.length}),void t.push.apply(t,u)}if(i instanceof ut){var s;do s=Lt(++e);while(!x(s));return i.mangled_name=s,!0}return n.screw_ie8&&i instanceof at?void t.push(i.definition()):void 0});this.walk(r),t.forEach(function(e){e.mangle(n)})}),fe.DEFMETHOD("compute_char_frequency",function(n){n=this._default_mangler_options(n);var e=new E(function(e){e instanceof lt?Lt.consider(e.print_to_string()):e instanceof ve?Lt.consider("return"):e instanceof ge?Lt.consider("throw"):e instanceof Ae?Lt.consider("continue"):e instanceof ye?Lt.consider("break"):e instanceof X?Lt.consider("debugger"):e instanceof G?Lt.consider(e.value):e instanceof oe?Lt.consider("while"):e instanceof ie?Lt.consider("do while"):e instanceof we?(Lt.consider("if"),e.alternative&&Lt.consider("else")):e instanceof Te?Lt.consider("var"):e instanceof $e?Lt.consider("const"):e instanceof le?Lt.consider("function"):e instanceof ae?Lt.consider("for"):e instanceof ue?Lt.consider("for in"):e instanceof Ee?Lt.consider("switch"):e instanceof Se?Lt.consider("case"):e instanceof Fe?Lt.consider("default"):e instanceof se?Lt.consider("with"):e instanceof Ke?Lt.consider("set"+e.key):e instanceof Je?Lt.consider("get"+e.key):e instanceof Ge?Lt.consider(e.key):e instanceof Ne?Lt.consider("new"):e instanceof ft?Lt.consider("this"):e instanceof Ce?Lt.consider("try"):e instanceof ke?Lt.consider("catch"):e instanceof xe?Lt.consider("finally"):e instanceof Ze&&e.unmangleable(n)?Lt.consider(e.name):e instanceof Pe||e instanceof Ue?Lt.consider(e.operator):e instanceof He&&Lt.consider(e.property)});this.walk(e),Lt.sort()});var Lt=function(){function n(){r=Object.create(null),t=i.split("").map(function(n){return n.charCodeAt(0)}),t.forEach(function(n){r[n]=0})}function e(n){var e="",r=54;do e+=String.fromCharCode(t[n%r]),n=Math.floor(n/r),r=64;while(n>0);return e}var t,r,i="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ$_0123456789";return e.consider=function(n){for(var e=n.length;--e>=0;){var t=n.charCodeAt(e);t in r&&++r[t]}},e.sort=function(){t=_(t,function(n,e){return F(n)&&!F(e)?1:F(e)&&!F(n)?-1:r[e]-r[n]})},e.reset=n,n(),e.get=function(){return t},e.freq=function(){return r},e}();fe.DEFMETHOD("scope_warnings",function(n){n=c(n,{undeclared:!1,unreferenced:!0,assign_to_global:!0,func_arguments:!0,nested_defuns:!0,eval:!0});var e=new E(function(t){if(n.undeclared&&t instanceof st&&t.undeclared()&&W.warn("Undeclared symbol: {name} [{file}:{line},{col}]",{name:t.name,file:t.start.file,line:t.start.line,col:t.start.col}),n.assign_to_global){var r=null;t instanceof Le&&t.left instanceof st?r=t.left:t instanceof ue&&t.init instanceof st&&(r=t.init),r&&(r.undeclared()||r.global()&&r.scope!==r.definition().scope)&&W.warn("{msg}: {name} [{file}:{line},{col}]",{msg:r.undeclared()?"Accidental global?":"Assignment to global",name:r.name,file:r.start.file,line:r.start.line,col:r.start.col})}n.eval&&t instanceof st&&t.undeclared()&&"eval"==t.name&&W.warn("Eval is used [{file}:{line},{col}]",t.start),n.unreferenced&&(t instanceof nt||t instanceof ut)&&t.unreferenced()&&W.warn("{type} {name} is declared but not referenced [{file}:{line},{col}]",{type:t instanceof ut?"Label":"Symbol",name:t.name,file:t.start.file,line:t.start.line,col:t.start.col}),n.func_arguments&&t instanceof le&&t.uses_arguments&&W.warn("arguments used in function {name} [{file}:{line},{col}]",{name:t.name?t.name.name:"anonymous",file:t.start.file,line:t.start.line,col:t.start.col}),n.nested_defuns&&t instanceof he&&!(e.parent()instanceof ce)&&W.warn('Function {name} declared in nested statement "{type}" [{file}:{line},{col}]',{name:t.name.name,type:e.parent().TYPE,file:t.start.file,line:t.start.line,col:t.start.col})});this.walk(e)}),function(){function n(n,e){n.DEFMETHOD("_codegen",e)}function e(n,e){n.DEFMETHOD("needs_parens",e)}function t(n){var e=n.parent();return e instanceof Pe?!0:e instanceof Ue&&!(e instanceof Le)?!0:e instanceof Me&&e.expression===this?!0:e instanceof Ve&&e.condition===this?!0:e instanceof qe&&e.expression===this?!0:void 0}function r(n,e,t){var r=n.length-1;n.forEach(function(n,i){n instanceof Q||(t.indent(),n.print(t),i==r&&e||(t.newline(),e&&t.newline()))})}function i(n,e){n.length>0?e.with_block(function(){r(n,!1,e)}):e.print("{}")}function o(n,e){if(e.option("bracketize"))return void h(n.body,e);if(!n.body)return e.force_semicolon();if(n.body instanceof ie&&!e.option("screw_ie8"))return void h(n.body,e);for(var t=n.body;;)if(t instanceof we){if(!t.alternative)return void h(n.body,e);t=t.alternative}else{if(!(t instanceof ne))break;t=t.body}s(n.body,e)}function a(n,e,t){if(t)try{n.walk(new E(function(n){if(n instanceof Ue&&"in"==n.operator)throw e})),n.print(e)}catch(r){if(r!==e)throw r;n.print(e,!0)}else n.print(e)}function u(n){return[92,47,46,43,42,63,40,41,91,93,123,125,36,94,58,124,33,10,13,0,65279,8232,8233].indexOf(n)<0}function s(n,e){e.option("bracketize")?!n||n instanceof Q?e.print("{}"):n instanceof Z?n.print(e):e.with_block(function(){e.indent(),n.print(e),e.newline()}):!n||n instanceof Q?e.force_semicolon():n.print(e)}function c(n){for(var e=n.stack(),t=e.length,r=e[--t],i=e[--t];t>0;){if(i instanceof Y&&i.body===r)return!0;if(!(i instanceof Re&&i.car===r||i instanceof Me&&i.expression===r&&!(i instanceof Ne)||i instanceof He&&i.expression===r||i instanceof ze&&i.expression===r||i instanceof Ve&&i.condition===r||i instanceof Ue&&i.left===r||i instanceof Ie&&i.expression===r))return!1;r=i,i=e[--t]}}function f(n,e){return 0==n.args.length&&!e.option("beautify")}function p(n){for(var e=n[0],t=e.length,r=1;r=0?r.push("0x"+n.toString(16).toLowerCase(),"0"+n.toString(8)):r.push("-0x"+(-n).toString(16).toLowerCase(),"-0"+(-n).toString(8)),(e=/^(.*?)(0+)$/.exec(n))&&r.push(e[1]+"e"+e[2].length)):(e=/^0?\.(0+)(.*)$/.exec(n))&&r.push(e[2]+"e-"+(e[1].length+e[2].length),t.substr(t.indexOf("."))),p(r)}function h(n,e){return n instanceof Z?void n.print(e):void e.with_block(function(){e.indent(),n.print(e),e.newline()})}function _(n,e){n.DEFMETHOD("add_source_map",function(n){e(this,n)})}function m(n,e){e.add_mapping(n.start)}W.DEFMETHOD("print",function(n,e){function t(){r.add_comments(n),r.add_source_map(n),i(r,n)}var r=this,i=r._codegen;n.push_node(r),e||r.needs_parens(n)?n.with_parens(t):t(),n.pop_node()}),W.DEFMETHOD("print_to_string",function(n){var e=j(n);return this.print(e),e.get()}),W.DEFMETHOD("add_comments",function(n){var e=n.option("comments"),t=this;if(e){var r=t.start;if(r&&!r._comments_dumped){r._comments_dumped=!0;var i=r.comments_before||[];t instanceof me&&t.value&&t.value.walk(new E(function(n){return n.start&&n.start.comments_before&&(i=i.concat(n.start.comments_before),n.start.comments_before=[]),n instanceof de||n instanceof We||n instanceof Ye?!0:void 0
})),e.test?i=i.filter(function(n){return e.test(n.value)}):"function"==typeof e&&(i=i.filter(function(n){return e(t,n)})),i.forEach(function(e){/comment[134]/.test(e.type)?(n.print("//"+e.value+"\n"),n.indent()):"comment2"==e.type&&(n.print("/*"+e.value+"*/"),r.nlb?(n.print("\n"),n.indent()):n.space())})}}}),e(W,function(){return!1}),e(de,function(n){return c(n)}),e(Ye,function(n){return c(n)}),e(Pe,function(n){var e=n.parent();return e instanceof qe&&e.expression===this}),e(Re,function(n){var e=n.parent();return e instanceof Me||e instanceof Pe||e instanceof Ue||e instanceof Oe||e instanceof qe||e instanceof We||e instanceof Xe||e instanceof Ve}),e(Ue,function(n){var e=n.parent();if(e instanceof Me&&e.expression===this)return!0;if(e instanceof Pe)return!0;if(e instanceof qe&&e.expression===this)return!0;if(e instanceof Ue){var t=e.operator,r=It[t],i=this.operator,o=It[i];if(r>o||r==o&&this===e.right)return!0}}),e(qe,function(n){var e=n.parent();if(e instanceof Ne&&e.expression===this)try{this.walk(new E(function(n){if(n instanceof Me)throw e}))}catch(t){if(t!==e)throw t;return!0}}),e(Me,function(n){var e,t=n.parent();return t instanceof Ne&&t.expression===this?!0:this.expression instanceof de&&t instanceof qe&&t.expression===this&&(e=n.parent(1))instanceof Le&&e.left===t}),e(Ne,function(n){var e=n.parent();return f(this,n)&&(e instanceof qe||e instanceof Me&&e.expression===this)?!0:void 0}),e(dt,function(n){var e=n.parent();return this.getValue()<0&&e instanceof qe&&e.expression===this?!0:void 0}),e(vt,function(n){var e=n.parent();return e instanceof qe&&e.expression===this?!0:void 0}),e(Le,t),e(Ve,t),n(G,function(n,e){e.print_string(n.value),e.semicolon()}),n(X,function(n,e){e.print("debugger"),e.semicolon()}),ne.DEFMETHOD("_do_print_body",function(n){s(this.body,n)}),n(Y,function(n,e){n.body.print(e),e.semicolon()}),n(fe,function(n,e){r(n.body,!0,e),e.print("")}),n(ee,function(n,e){n.label.print(e),e.colon(),n.body.print(e)}),n(K,function(n,e){n.body.print(e),e.semicolon()}),n(Z,function(n,e){i(n.body,e)}),n(Q,function(n,e){e.semicolon()}),n(ie,function(n,e){e.print("do"),e.space(),n._do_print_body(e),e.space(),e.print("while"),e.space(),e.with_parens(function(){n.condition.print(e)}),e.semicolon()}),n(oe,function(n,e){e.print("while"),e.space(),e.with_parens(function(){n.condition.print(e)}),e.space(),n._do_print_body(e)}),n(ae,function(n,e){e.print("for"),e.space(),e.with_parens(function(){!n.init||n.init instanceof Q?e.print(";"):(n.init instanceof Be?n.init.print(e):a(n.init,e,!0),e.print(";"),e.space()),n.condition?(n.condition.print(e),e.print(";"),e.space()):e.print(";"),n.step&&n.step.print(e)}),e.space(),n._do_print_body(e)}),n(ue,function(n,e){e.print("for"),e.space(),e.with_parens(function(){n.init.print(e),e.space(),e.print("in"),e.space(),n.object.print(e)}),e.space(),n._do_print_body(e)}),n(se,function(n,e){e.print("with"),e.space(),e.with_parens(function(){n.expression.print(e)}),e.space(),n._do_print_body(e)}),le.DEFMETHOD("_do_print",function(n,e){var t=this;e||n.print("function"),t.name&&(n.space(),t.name.print(n)),n.with_parens(function(){t.argnames.forEach(function(e,t){t&&n.comma(),e.print(n)})}),n.space(),i(t.body,n)}),n(le,function(n,e){n._do_print(e)}),me.DEFMETHOD("_do_print",function(n,e){n.print(e),this.value&&(n.space(),this.value.print(n)),n.semicolon()}),n(ve,function(n,e){n._do_print(e,"return")}),n(ge,function(n,e){n._do_print(e,"throw")}),be.DEFMETHOD("_do_print",function(n,e){n.print(e),this.label&&(n.space(),this.label.print(n)),n.semicolon()}),n(ye,function(n,e){n._do_print(e,"break")}),n(Ae,function(n,e){n._do_print(e,"continue")}),n(we,function(n,e){e.print("if"),e.space(),e.with_parens(function(){n.condition.print(e)}),e.space(),n.alternative?(o(n,e),e.space(),e.print("else"),e.space(),s(n.alternative,e)):n._do_print_body(e)}),n(Ee,function(n,e){e.print("switch"),e.space(),e.with_parens(function(){n.expression.print(e)}),e.space(),n.body.length>0?e.with_block(function(){n.body.forEach(function(n,t){t&&e.newline(),e.indent(!0),n.print(e)})}):e.print("{}")}),De.DEFMETHOD("_do_print_body",function(n){this.body.length>0&&(n.newline(),this.body.forEach(function(e){n.indent(),e.print(n),n.newline()}))}),n(Fe,function(n,e){e.print("default:"),n._do_print_body(e)}),n(Se,function(n,e){e.print("case"),e.space(),n.expression.print(e),e.print(":"),n._do_print_body(e)}),n(Ce,function(n,e){e.print("try"),e.space(),i(n.body,e),n.bcatch&&(e.space(),n.bcatch.print(e)),n.bfinally&&(e.space(),n.bfinally.print(e))}),n(ke,function(n,e){e.print("catch"),e.space(),e.with_parens(function(){n.argname.print(e)}),e.space(),i(n.body,e)}),n(xe,function(n,e){e.print("finally"),e.space(),i(n.body,e)}),Be.DEFMETHOD("_do_print",function(n,e){n.print(e),n.space(),this.definitions.forEach(function(e,t){t&&n.comma(),e.print(n)});var t=n.parent(),r=t instanceof ae||t instanceof ue,i=r&&t.init===this;i||n.semicolon()}),n(Te,function(n,e){n._do_print(e,"var")}),n($e,function(n,e){n._do_print(e,"const")}),n(Oe,function(n,e){if(n.name.print(e),n.value){e.space(),e.print("="),e.space();var t=e.parent(1),r=t instanceof ae||t instanceof ue;a(n.value,e,r)}}),n(Me,function(n,e){n.expression.print(e),n instanceof Ne&&f(n,e)||e.with_parens(function(){n.args.forEach(function(n,t){t&&e.comma(),n.print(e)})})}),n(Ne,function(n,e){e.print("new"),e.space(),Me.prototype._codegen(n,e)}),Re.DEFMETHOD("_do_print",function(n){this.car.print(n),this.cdr&&(n.comma(),n.should_break()&&(n.newline(),n.indent()),this.cdr.print(n))}),n(Re,function(n,e){n._do_print(e)}),n(He,function(n,e){var t=n.expression;t.print(e),t instanceof dt&&t.getValue()>=0&&(/[xa-f.]/i.test(e.last())||e.print(".")),e.print("."),e.add_mapping(n.end),e.print_name(n.property)}),n(ze,function(n,e){n.expression.print(e),e.print("["),n.property.print(e),e.print("]")}),n(je,function(n,e){var t=n.operator;e.print(t),(/^[a-z]/i.test(t)||/[+-]$/.test(t)&&n.expression instanceof je&&/^[+-]/.test(n.expression.operator))&&e.space(),n.expression.print(e)}),n(Ie,function(n,e){n.expression.print(e),e.print(n.operator)}),n(Ue,function(n,e){n.left.print(e),e.space(),e.print(n.operator),"<"==n.operator&&n.right instanceof je&&"!"==n.right.operator&&n.right.expression instanceof je&&"--"==n.right.expression.operator?e.print(" "):e.space(),n.right.print(e)}),n(Ve,function(n,e){n.condition.print(e),e.space(),e.print("?"),e.space(),n.consequent.print(e),e.space(),e.colon(),n.alternative.print(e)}),n(We,function(n,e){e.with_square(function(){var t=n.elements,r=t.length;r>0&&e.space(),t.forEach(function(n,t){t&&e.comma(),n.print(e),t===r-1&&n instanceof bt&&e.comma()}),r>0&&e.space()})}),n(Ye,function(n,e){n.properties.length>0?e.with_block(function(){n.properties.forEach(function(n,t){t&&(e.print(","),e.newline()),e.indent(),n.print(e)}),e.newline()}):e.print("{}")}),n(Ge,function(n,e){var t=n.key;e.option("quote_keys")?e.print_string(t+""):("number"==typeof t||!e.option("beautify")&&+t+""==t)&&parseFloat(t)>=0?e.print(d(t)):(St(t)?e.option("screw_ie8"):$(t))?e.print_name(t):e.print_string(t),e.colon(),n.value.print(e)}),n(Ke,function(n,e){e.print("set"),e.space(),n.key.print(e),n.value._do_print(e,!0)}),n(Je,function(n,e){e.print("get"),e.space(),n.key.print(e),n.value._do_print(e,!0)}),n(Ze,function(n,e){var t=n.definition();e.print_name(t?t.mangled_name||t.name:n.name)}),n(gt,function(n,e){e.print("void 0")}),n(bt,l),n(yt,function(n,e){e.print("1/0")}),n(vt,function(n,e){e.print("0/0")}),n(ft,function(n,e){e.print("this")}),n(lt,function(n,e){e.print(n.getValue())}),n(pt,function(n,e){e.print_string(n.getValue())}),n(dt,function(n,e){e.print(d(n.getValue()))}),n(ht,function(n,e){var t=n.getValue().toString();e.option("ascii_only")?t=e.to_ascii(t):e.option("unescape_regexps")&&(t=t.split("\\\\").map(function(n){return n.replace(/\\u[0-9a-fA-F]{4}|\\x[0-9a-fA-F]{2}/g,function(n){var e=parseInt(n.substr(2),16);return u(e)?String.fromCharCode(e):n})}).join("\\\\")),e.print(t);var r=e.parent();r instanceof Ue&&/^in/.test(r.operator)&&r.left===n&&e.print(" ")}),_(W,l),_(G,m),_(X,m),_(Ze,m),_(_e,m),_(ne,m),_(ee,l),_(le,m),_(Ee,m),_(De,m),_(Z,m),_(fe,l),_(Ne,m),_(Ce,m),_(ke,m),_(xe,m),_(Be,m),_(lt,m),_(Xe,function(n,e){e.add_mapping(n.start,n.key)})}(),I.prototype=new z,f(I.prototype,{option:function(n){return this.options[n]},warn:function(){this.options.warnings&&W.warn.apply(W,arguments)},before:function(n,e){if(n._squeezed)return n;var t=!1;return n instanceof ce&&(n=n.hoist_declarations(this),t=!0),e(n,this),n=n.optimize(this),t&&n instanceof ce&&(n.drop_unused(this),e(n,this)),n._squeezed=!0,n}}),function(){function n(n,e){n.DEFMETHOD("optimize",function(n){var t=this;if(t._optimized)return t;var r=e(t,n);return r._optimized=!0,r===t?r:r.transform(n)})}function e(n,e,t){return t||(t={}),e&&(t.start||(t.start=e.start),t.end||(t.end=e.end)),new n(t)}function t(n,t,r){if(t instanceof W)return t.transform(n);switch(typeof t){case"string":return e(pt,r,{value:t}).optimize(n);case"number":return e(isNaN(t)?vt:dt,r,{value:t}).optimize(n);case"boolean":return e(t?Et:wt,r).optimize(n);case"undefined":return e(gt,r).optimize(n);default:if(null===t)return e(mt,r).optimize(n);if(t instanceof RegExp)return e(ht,r).optimize(n);throw new Error(d("Can't handle constant of type: {type}",{type:typeof t}))}}function r(n){if(null===n)return[];if(n instanceof Z)return n.body;if(n instanceof Q)return[];if(n instanceof Y)return[n];throw new Error("Can't convert thing to statement array")}function i(n){return null===n?!0:n instanceof Q?!0:n instanceof Z?0==n.body.length:!1}function u(n){return n instanceof Ee?n:(n instanceof ae||n instanceof ue||n instanceof re)&&n.body instanceof Z?n.body:n}function s(n,t){function i(n){function r(n,t){return e(K,n,{body:e(Le,n,{operator:"=",left:e(He,t,{expression:e(st,t,t),property:"$inject"}),right:e(We,n,{elements:n.argnames.map(function(n){return e(pt,n,{value:n.name})})})})})}return n.reduce(function(n,e){n.push(e);var i=e.start,o=i.comments_before;if(o&&o.length>0){var a=o.pop();/@ngInject/.test(a.value)&&(e instanceof he?n.push(r(e,e.name)):e instanceof Be?e.definitions.forEach(function(e){e.value&&e.value instanceof le&&n.push(r(e.value,e.name))}):t.warn("Unknown statement marked with @ngInject [{file}:{line},{col}]",i))}return n},[])}function o(n){var e=[];return n.reduce(function(n,t){return t instanceof Z?(_=!0,n.push.apply(n,o(t.body))):t instanceof Q?_=!0:t instanceof G?e.indexOf(t.value)<0?(n.push(t),e.push(t.value)):_=!0:n.push(t),n},[])}function a(n,t){var i=t.self(),o=i instanceof le,a=[];n:for(var s=n.length;--s>=0;){var c=n[s];switch(!0){case o&&c instanceof ve&&!c.value&&0==a.length:_=!0;continue n;case c instanceof we:if(c.body instanceof ve){if((o&&0==a.length||a[0]instanceof ve&&!a[0].value)&&!c.body.value&&!c.alternative){_=!0;var f=e(K,c.condition,{body:c.condition});a.unshift(f);continue n}if(a[0]instanceof ve&&c.body.value&&a[0].value&&!c.alternative){_=!0,c=c.clone(),c.alternative=a[0],a[0]=c.transform(t);continue n}if((0==a.length||a[0]instanceof ve)&&c.body.value&&!c.alternative&&o){_=!0,c=c.clone(),c.alternative=a[0]||e(ve,c,{value:e(gt,c)}),a[0]=c.transform(t);continue n}if(!c.body.value&&o){_=!0,c=c.clone(),c.condition=c.condition.negate(t),c.body=e(Z,c,{body:r(c.alternative).concat(a)}),c.alternative=null,a=[c.transform(t)];continue n}if(1==a.length&&o&&a[0]instanceof K&&(!c.alternative||c.alternative instanceof K)){_=!0,a.push(e(ve,a[0],{value:e(gt,a[0])}).transform(t)),a=r(c.alternative).concat(a),a.unshift(c);continue n}}var l=m(c.body),p=l instanceof be?t.loopcontrol_target(l.label):null;if(l&&(l instanceof ve&&!l.value&&o||l instanceof Ae&&i===u(p)||l instanceof ye&&p instanceof Z&&i===p)){l.label&&h(l.label.thedef.references,l),_=!0;var d=r(c.body).slice(0,-1);c=c.clone(),c.condition=c.condition.negate(t),c.body=e(Z,c,{body:r(c.alternative).concat(a)}),c.alternative=e(Z,c,{body:d}),a=[c.transform(t)];continue n}var l=m(c.alternative),p=l instanceof be?t.loopcontrol_target(l.label):null;if(l&&(l instanceof ve&&!l.value&&o||l instanceof Ae&&i===u(p)||l instanceof ye&&p instanceof Z&&i===p)){l.label&&h(l.label.thedef.references,l),_=!0,c=c.clone(),c.body=e(Z,c.body,{body:r(c.body).concat(a)}),c.alternative=e(Z,c.alternative,{body:r(c.alternative).slice(0,-1)}),a=[c.transform(t)];continue n}a.unshift(c);break;default:a.unshift(c)}}return a}function s(n,e){var t=!1,r=n.length,i=e.self();return n=n.reduce(function(n,r){if(t)c(e,r,n);else{if(r instanceof be){var o=e.loopcontrol_target(r.label);r instanceof ye&&o instanceof Z&&u(o)===i||r instanceof Ae&&u(o)===i?r.label&&h(r.label.thedef.references,r):n.push(r)}else n.push(r);m(r)&&(t=!0)}return n},[]),_=n.length!=r,n}function f(n,t){function r(){i=Re.from_array(i),i&&o.push(e(K,i,{body:i})),i=[]}if(n.length<2)return n;var i=[],o=[];return n.forEach(function(n){n instanceof K?i.push(n.body):(r(),o.push(n))}),r(),o=l(o,t),_=o.length!=n.length,o}function l(n,t){function r(n){i.pop();var e=o.body;return e instanceof Re?e.add(n):e=Re.cons(e,n),e.transform(t)}var i=[],o=null;return n.forEach(function(n){if(o)if(n instanceof ae){var t={};try{o.body.walk(new E(function(n){if(n instanceof Ue&&"in"==n.operator)throw t})),!n.init||n.init instanceof Be?n.init||(n.init=o.body,i.pop()):n.init=r(n.init)}catch(a){if(a!==t)throw a}}else n instanceof we?n.condition=r(n.condition):n instanceof se?n.expression=r(n.expression):n instanceof me&&n.value?n.value=r(n.value):n instanceof me?n.value=r(e(gt,n)):n instanceof Ee&&(n.expression=r(n.expression));i.push(n),o=n instanceof K?n:null}),i}function p(n){var e=null;return n.reduce(function(n,t){return t instanceof Be&&e&&e.TYPE==t.TYPE?(e.definitions=e.definitions.concat(t.definitions),_=!0):t instanceof ae&&e instanceof Be&&(!t.init||t.init.TYPE==e.TYPE)?(_=!0,n.pop(),t.init?t.init.definitions=e.definitions.concat(t.init.definitions):t.init=e,n.push(t),e=t):(e=t,n.push(t)),n},[])}function d(n){n.forEach(function(n){n instanceof K&&(n.body=function t(n){return n.transform(new z(function(n){if(n instanceof Me&&n.expression instanceof de)return e(je,n,{operator:"!",expression:n});if(n instanceof Me)n.expression=t(n.expression);else if(n instanceof Re)n.car=t(n.car);else if(n instanceof Ve){var r=t(n.condition);if(r!==n.condition){n.condition=r;var i=n.consequent;n.consequent=n.alternative,n.alternative=i}}return n}))}(n.body))})}var _;do _=!1,t.option("angular")&&(n=i(n)),n=o(n),t.option("dead_code")&&(n=s(n,t)),t.option("if_return")&&(n=a(n,t)),t.option("sequences")&&(n=f(n,t)),t.option("join_vars")&&(n=p(n,t));while(_);return t.option("negate_iife")&&d(n,t),n}function c(n,e,t){n.warn("Dropping unreachable code [{file}:{line},{col}]",e.start),e.walk(new E(function(e){return e instanceof Be?(n.warn("Declarations in unreachable code! [{file}:{line},{col}]",e.start),e.remove_initializers(),t.push(e),!0):e instanceof he?(t.push(e),!0):e instanceof ce?!0:void 0}))}function f(n,e){return n.print_to_string().length>e.print_to_string().length?e:n}function m(n){return n&&n.aborts()}function v(n,t){function i(i){i=r(i),n.body instanceof Z?(n.body=n.body.clone(),n.body.body=i.concat(n.body.body.slice(1)),n.body=n.body.transform(t)):n.body=e(Z,n.body,{body:i}).transform(t),v(n,t)}var o=n.body instanceof Z?n.body.body[0]:n.body;o instanceof we&&(o.body instanceof ye&&t.loopcontrol_target(o.body.label)===n?(n.condition=n.condition?e(Ue,n.condition,{left:n.condition,operator:"&&",right:o.condition.negate(t)}):o.condition.negate(t),i(o.alternative)):o.alternative instanceof ye&&t.loopcontrol_target(o.alternative.label)===n&&(n.condition=n.condition?e(Ue,n.condition,{left:n.condition,operator:"&&",right:o.condition}):o.condition,i(o.body)))}function A(n,e){var t=e.option("pure_getters");e.options.pure_getters=!1;var r=n.has_side_effects(e);return e.options.pure_getters=t,r}function w(n,t){return t.option("booleans")&&t.in_boolean_context()?e(Et,n):n}n(W,function(n){return n}),W.DEFMETHOD("equivalent_to",function(n){return this.print_to_string()==n.print_to_string()}),function(n){var e=["!","delete"],t=["in","instanceof","==","!=","===","!==","<","<=",">=",">"];n(W,function(){return!1}),n(je,function(){return o(this.operator,e)}),n(Ue,function(){return o(this.operator,t)||("&&"==this.operator||"||"==this.operator)&&this.left.is_boolean()&&this.right.is_boolean()}),n(Ve,function(){return this.consequent.is_boolean()&&this.alternative.is_boolean()}),n(Le,function(){return"="==this.operator&&this.right.is_boolean()}),n(Re,function(){return this.cdr.is_boolean()}),n(Et,function(){return!0}),n(wt,function(){return!0})}(function(n,e){n.DEFMETHOD("is_boolean",e)}),function(n){n(W,function(){return!1}),n(pt,function(){return!0}),n(je,function(){return"typeof"==this.operator}),n(Ue,function(n){return"+"==this.operator&&(this.left.is_string(n)||this.right.is_string(n))}),n(Le,function(n){return("="==this.operator||"+="==this.operator)&&this.right.is_string(n)}),n(Re,function(n){return this.cdr.is_string(n)}),n(Ve,function(n){return this.consequent.is_string(n)&&this.alternative.is_string(n)}),n(Me,function(n){return n.option("unsafe")&&this.expression instanceof st&&"String"==this.expression.name&&this.expression.undeclared()})}(function(n,e){n.DEFMETHOD("is_string",e)}),function(n){function e(n,e){if(!e)throw new Error("Compressor must be passed");return n._eval(e)}W.DEFMETHOD("evaluate",function(e){if(!e.option("evaluate"))return[this];try{var r=this._eval(e);return[f(t(e,r,this),this),r]}catch(i){if(i!==n)throw i;return[this]}}),n(Y,function(){throw new Error(d("Cannot evaluate a statement [{file}:{line},{col}]",this.start))}),n(de,function(){throw n}),n(W,function(){throw n}),n(lt,function(){return this.getValue()}),n(je,function(t){var r=this.expression;switch(this.operator){case"!":return!e(r,t);case"typeof":if(r instanceof de)return"function";if(r=e(r,t),r instanceof RegExp)throw n;return typeof r;case"void":return void e(r,t);case"~":return~e(r,t);case"-":if(r=e(r,t),0===r)throw n;return-r;case"+":return+e(r,t)}throw n}),n(Ue,function(t){var r=this.left,i=this.right;switch(this.operator){case"&&":return e(r,t)&&e(i,t);case"||":return e(r,t)||e(i,t);case"|":return e(r,t)|e(i,t);case"&":return e(r,t)&e(i,t);case"^":return e(r,t)^e(i,t);case"+":return e(r,t)+e(i,t);case"*":return e(r,t)*e(i,t);case"/":return e(r,t)/e(i,t);case"%":return e(r,t)%e(i,t);case"-":return e(r,t)-e(i,t);case"<<":return e(r,t)<>":return e(r,t)>>e(i,t);case">>>":return e(r,t)>>>e(i,t);case"==":return e(r,t)==e(i,t);case"===":return e(r,t)===e(i,t);case"!=":return e(r,t)!=e(i,t);case"!==":return e(r,t)!==e(i,t);case"<":return e(r,t)":return e(r,t)>e(i,t);case">=":return e(r,t)>=e(i,t);case"in":return e(r,t)in e(i,t);case"instanceof":return e(r,t)instanceof e(i,t)}throw n}),n(Ve,function(n){return e(this.condition,n)?e(this.consequent,n):e(this.alternative,n)}),n(st,function(t){var r=this.definition();if(r&&r.constant&&r.init)return e(r.init,t);throw n}),n(He,function(t){if(t.option("unsafe")&&"length"==this.property){var r=e(this.expression,t);if("string"==typeof r)return r.length}throw n})}(function(n,e){n.DEFMETHOD("_eval",e)}),function(n){function t(n){return e(je,n,{operator:"!",expression:n})}n(W,function(){return t(this)}),n(Y,function(){throw new Error("Cannot negate a statement")}),n(de,function(){return t(this)}),n(je,function(){return"!"==this.operator?this.expression:t(this)}),n(Re,function(n){var e=this.clone();return e.cdr=e.cdr.negate(n),e}),n(Ve,function(n){var e=this.clone();return e.consequent=e.consequent.negate(n),e.alternative=e.alternative.negate(n),f(t(this),e)}),n(Ue,function(n){var e=this.clone(),r=this.operator;if(n.option("unsafe_comps"))switch(r){case"<=":return e.operator=">",e;case"<":return e.operator=">=",e;case">=":return e.operator="<",e;case">":return e.operator="<=",e}switch(r){case"==":return e.operator="!=",e;case"!=":return e.operator="==",e;case"===":return e.operator="!==",e;case"!==":return e.operator="===",e;case"&&":return e.operator="||",e.left=e.left.negate(n),e.right=e.right.negate(n),f(t(this),e);case"||":return e.operator="&&",e.left=e.left.negate(n),e.right=e.right.negate(n),f(t(this),e)}return t(this)})}(function(n,e){n.DEFMETHOD("negate",function(n){return e.call(this,n)})}),function(n){n(W,function(){return!0}),n(Q,function(){return!1}),n(lt,function(){return!1}),n(ft,function(){return!1}),n(Me,function(n){var e=n.option("pure_funcs");return e?e.indexOf(this.expression.print_to_string())<0:!0}),n(J,function(n){for(var e=this.body.length;--e>=0;)if(this.body[e].has_side_effects(n))return!0;return!1}),n(K,function(n){return this.body.has_side_effects(n)}),n(he,function(){return!0}),n(de,function(){return!1}),n(Ue,function(n){return this.left.has_side_effects(n)||this.right.has_side_effects(n)}),n(Le,function(){return!0}),n(Ve,function(n){return this.condition.has_side_effects(n)||this.consequent.has_side_effects(n)||this.alternative.has_side_effects(n)}),n(Pe,function(n){return"delete"==this.operator||"++"==this.operator||"--"==this.operator||this.expression.has_side_effects(n)}),n(st,function(){return!1}),n(Ye,function(n){for(var e=this.properties.length;--e>=0;)if(this.properties[e].has_side_effects(n))return!0;return!1}),n(Xe,function(n){return this.value.has_side_effects(n)}),n(We,function(n){for(var e=this.elements.length;--e>=0;)if(this.elements[e].has_side_effects(n))return!0;return!1}),n(He,function(n){return n.option("pure_getters")?this.expression.has_side_effects(n):!0}),n(ze,function(n){return n.option("pure_getters")?this.expression.has_side_effects(n)||this.property.has_side_effects(n):!0}),n(qe,function(n){return!n.option("pure_getters")}),n(Re,function(n){return this.car.has_side_effects(n)||this.cdr.has_side_effects(n)})}(function(n,e){n.DEFMETHOD("has_side_effects",e)}),function(n){function e(){var n=this.body.length;return n>0&&m(this.body[n-1])}n(Y,function(){return null}),n(_e,function(){return this}),n(Z,e),n(De,e),n(we,function(){return this.alternative&&m(this.body)&&m(this.alternative)})}(function(n,e){n.DEFMETHOD("aborts",e)}),n(G,function(n){return n.scope.has_directive(n.value)!==n.scope?e(Q,n):n}),n(X,function(n,t){return t.option("drop_debugger")?e(Q,n):n}),n(ee,function(n,t){return n.body instanceof ye&&t.loopcontrol_target(n.body.label)===n.body?e(Q,n):0==n.label.references.length?n.body:n}),n(J,function(n,e){return n.body=s(n.body,e),n}),n(Z,function(n,t){switch(n.body=s(n.body,t),n.body.length){case 1:return n.body[0];case 0:return e(Q,n)}return n}),ce.DEFMETHOD("drop_unused",function(n){var t=this;if(n.option("unused")&&!(t instanceof fe)&&!t.uses_eval){var r=[],i=new y,a=this,u=new E(function(e,o){if(e!==t){if(e instanceof he)return i.add(e.name.name,e),!0;if(e instanceof Be&&a===t)return e.definitions.forEach(function(e){e.value&&(i.add(e.name.name,e.value),e.value.has_side_effects(n)&&e.value.walk(u))}),!0;if(e instanceof st)return p(r,e.definition()),!0;if(e instanceof ce){var s=a;return a=e,o(),a=s,!0}}});t.walk(u);for(var s=0;s=0;){var l=s[f];if(!l.unreferenced())break;s.pop(),n.warn("Dropping unused function argument {name} [{file}:{line},{col}]",{name:l.name,file:l.start.file,line:l.start.line,col:l.start.col})}if(i instanceof he&&i!==t)return o(i.name.definition(),r)?i:(n.warn("Dropping unused function {name} [{file}:{line},{col}]",{name:i.name.name,file:i.name.start.file,line:i.name.start.line,col:i.name.start.col}),e(Q,i));if(i instanceof Be&&!(c.parent()instanceof ue)){var p=i.definitions.filter(function(e){if(o(e.name.definition(),r))return!0;var t={name:e.name.name,file:e.name.start.file,line:e.name.start.line,col:e.name.start.col};return e.value&&e.value.has_side_effects(n)?(e._unused_side_effects=!0,n.warn("Side effects in initialization of unused variable {name} [{file}:{line},{col}]",t),!0):(n.warn("Dropping unused variable {name} [{file}:{line},{col}]",t),!1)});p=_(p,function(n,e){return!n.value&&e.value?-1:!e.value&&n.value?1:0});for(var d=[],f=0;f0&&(d.push(h.value),h.value=Re.from_array(d),d=[]),++f)}return d=d.length>0?e(Z,i,{body:[e(K,i,{body:Re.from_array(d)})]}):null,0!=p.length||d?0==p.length?d:(i.definitions=p,d&&(d.body.unshift(i),i=d),i):e(Q,i)}if(i instanceof ae&&(a(i,this),i.init instanceof Z)){var m=i.init.body.slice(0,-1);return i.init=i.init.body.slice(-1)[0].body,m.push(i),u?V.splice(m):e(Z,i,{body:m})}return i instanceof ce&&i!==t?i:void 0});t.transform(c)}}),ce.DEFMETHOD("hoist_declarations",function(n){var t=n.option("hoist_funs"),r=n.option("hoist_vars"),i=this;if(t||r){var o=[],u=[],s=new y,c=0,f=0;i.walk(new E(function(n){return n instanceof ce&&n!==i?!0:n instanceof Te?(++f,!0):void 0})),r=r&&f>1;var l=new z(function(n){if(n!==i){if(n instanceof G)return o.push(n),e(Q,n);if(n instanceof he&&t)return u.push(n),e(Q,n);if(n instanceof Te&&r){n.definitions.forEach(function(n){s.set(n.name.name,n),++c});var a=n.to_assignments(),f=l.parent();return f instanceof ue&&f.init===n?null==a?n.definitions[0].name:a:f instanceof ae&&f.init===n?a:a?e(K,n,{body:a}):e(Q,n)}if(n instanceof ce)return n}});if(i=i.transform(l),c>0){var p=[];if(s.each(function(n,e){i instanceof le&&a(function(e){return e.name==n.name.name},i.argnames)?s.del(e):(n=n.clone(),n.value=null,p.push(n),s.set(e,n))}),p.length>0){for(var d=0;d1){if(r[1])return e(ae,n,{body:n.body});if(n instanceof oe&&t.option("dead_code")){var i=[];return c(t,n.body,i),e(Z,n,{body:i})}}return n}),n(oe,function(n,t){return t.option("loops")?(n=re.prototype.optimize.call(n,t),n instanceof oe&&(v(n,t),n=e(ae,n,n).transform(t)),n):n}),n(ae,function(n,t){var r=n.condition;if(r&&(r=r.evaluate(t),n.condition=r[0]),!t.option("loops"))return n;if(r&&r.length>1&&!r[1]&&t.option("dead_code")){var i=[];return n.init instanceof Y?i.push(n.init):n.init&&i.push(e(K,n.init,{body:n.init})),c(t,n.body,i),e(Z,n,{body:i})}return v(n,t),n}),n(we,function(n,t){if(!t.option("conditionals"))return n;var r=n.condition.evaluate(t);if(n.condition=r[0],r.length>1)if(r[1]){if(t.warn("Condition always true [{file}:{line},{col}]",n.condition.start),t.option("dead_code")){var o=[];return n.alternative&&c(t,n.alternative,o),o.push(n.body),e(Z,n,{body:o}).transform(t)}}else if(t.warn("Condition always false [{file}:{line},{col}]",n.condition.start),t.option("dead_code")){var o=[];return c(t,n.body,o),n.alternative&&o.push(n.alternative),e(Z,n,{body:o}).transform(t)}i(n.alternative)&&(n.alternative=null);var a=n.condition.negate(t),u=f(n.condition,a)===a;if(n.alternative&&u){u=!1,n.condition=a;var s=n.body;n.body=n.alternative||e(Q),n.alternative=s}if(i(n.body)&&i(n.alternative))return e(K,n.condition,{body:n.condition}).transform(t);if(n.body instanceof K&&n.alternative instanceof K)return e(K,n,{body:e(Ve,n,{condition:n.condition,consequent:n.body.body,alternative:n.alternative.body})}).transform(t);if(i(n.alternative)&&n.body instanceof K)return u?e(K,n,{body:e(Ue,n,{operator:"||",left:a,right:n.body.body})}).transform(t):e(K,n,{body:e(Ue,n,{operator:"&&",left:n.condition,right:n.body.body})}).transform(t);if(n.body instanceof Q&&n.alternative&&n.alternative instanceof K)return e(K,n,{body:e(Ue,n,{operator:"||",left:n.condition,right:n.alternative.body})}).transform(t);if(n.body instanceof me&&n.alternative instanceof me&&n.body.TYPE==n.alternative.TYPE)return e(n.body.CTOR,n,{value:e(Ve,n,{condition:n.condition,consequent:n.body.value||e(gt,n.body).optimize(t),alternative:n.alternative.value||e(gt,n.alternative).optimize(t)})}).transform(t);if(n.body instanceof we&&!n.body.alternative&&!n.alternative&&(n.condition=e(Ue,n.condition,{operator:"&&",left:n.condition,right:n.body.condition}).transform(t),n.body=n.body.body),m(n.body)&&n.alternative){var l=n.alternative;return n.alternative=null,e(Z,n,{body:[n,l]}).transform(t)}if(m(n.alternative)){var p=n.body;return n.body=n.alternative,n.condition=u?a:n.condition.negate(t),n.alternative=null,e(Z,n,{body:[n,p]}).transform(t)}return n}),n(Ee,function(n,t){if(0==n.body.length&&t.option("conditionals"))return e(K,n,{body:n.expression}).transform(t);for(;;){var r=n.body[n.body.length-1];if(r){var i=r.body[r.body.length-1];if(i instanceof ye&&u(t.loopcontrol_target(i.label))===n&&r.body.pop(),r instanceof Fe&&0==r.body.length){n.body.pop();continue}}break}var o=n.expression.evaluate(t);n:if(2==o.length)try{if(n.expression=o[0],!t.option("dead_code"))break n;var a=o[1],s=!1,c=!1,f=!1,l=!1,p=!1,d=new z(function(r,i,o){if(r instanceof le||r instanceof K)return r;if(r instanceof Ee&&r===n)return r=r.clone(),i(r,this),p?r:e(Z,r,{body:r.body.reduce(function(n,e){return n.concat(e.body)},[])}).transform(t);if(r instanceof we||r instanceof Ce){var u=s;return s=!c,i(r,this),s=u,r}if(r instanceof ne||r instanceof Ee){var u=c;return c=!0,i(r,this),c=u,r}if(r instanceof ye&&this.loopcontrol_target(r.label)===n)return s?(p=!0,r):c?r:(l=!0,o?V.skip:e(Q,r));if(r instanceof De&&this.parent()===n){if(l)return V.skip;if(r instanceof Se){var d=r.expression.evaluate(t);if(d.length<2)throw n;return d[1]===a||f?(f=!0,m(r)&&(l=!0),i(r,this),r):V.skip}return i(r,this),r}});d.stack=t.stack.slice(),n=n.transform(d)}catch(h){if(h!==n)throw h}return n}),n(Se,function(n,e){return n.body=s(n.body,e),n}),n(Ce,function(n,e){return n.body=s(n.body,e),n}),Be.DEFMETHOD("remove_initializers",function(){this.definitions.forEach(function(n){n.value=null})}),Be.DEFMETHOD("to_assignments",function(){var n=this.definitions.reduce(function(n,t){if(t.value){var r=e(st,t.name,t.name);n.push(e(Le,t,{operator:"=",left:r,right:t.value}))}return n},[]);return 0==n.length?null:Re.from_array(n)}),n(Be,function(n){return 0==n.definitions.length?e(Q,n):n}),n(de,function(n,e){return n=le.prototype.optimize.call(n,e),e.option("unused")&&n.name&&n.name.unreferenced()&&(n.name=null),n}),n(Me,function(n,r){if(r.option("unsafe")){var i=n.expression;if(i instanceof st&&i.undeclared())switch(i.name){case"Array":if(1!=n.args.length)return e(We,n,{elements:n.args}).transform(r);break;case"Object":if(0==n.args.length)return e(Ye,n,{properties:[]});break;case"String":if(0==n.args.length)return e(pt,n,{value:""});if(n.args.length<=1)return e(Ue,n,{left:n.args[0],operator:"+",right:e(pt,n,{value:""})}).transform(r);break;case"Number":if(0==n.args.length)return e(dt,n,{value:0});if(1==n.args.length)return e(je,n,{expression:n.args[0],operator:"+"}).transform(r);case"Boolean":if(0==n.args.length)return e(wt,n);if(1==n.args.length)return e(je,n,{expression:e(je,null,{expression:n.args[0],operator:"!"}),operator:"!"}).transform(r);break;case"Function":if(b(n.args,function(n){return n instanceof pt}))try{var o="(function("+n.args.slice(0,-1).map(function(n){return n.value}).join(",")+"){"+n.args[n.args.length-1].value+"})()",a=H(o);
a.figure_out_scope({screw_ie8:r.option("screw_ie8")});var u=new I(r.options);a=a.transform(u),a.figure_out_scope({screw_ie8:r.option("screw_ie8")}),a.mangle_names();var s;try{a.walk(new E(function(n){if(n instanceof le)throw s=n,a}))}catch(c){if(c!==a)throw c}var l=s.argnames.map(function(t,r){return e(pt,n.args[r],{value:t.print_to_string()})}),o=j();return Z.prototype._codegen.call(s,s,o),o=o.toString().replace(/^\{|\}$/g,""),l.push(e(pt,n.args[n.args.length-1],{value:o})),n.args=l,n}catch(c){if(!(c instanceof M))throw console.log(c),c;r.warn("Error parsing code passed to new Function [{file}:{line},{col}]",n.args[n.args.length-1].start),r.warn(c.toString())}}else{if(i instanceof He&&"toString"==i.property&&0==n.args.length)return e(Ue,n,{left:e(pt,n,{value:""}),operator:"+",right:i.expression}).transform(r);if(i instanceof He&&i.expression instanceof We&&"join"==i.property){var p=0==n.args.length?",":n.args[0].evaluate(r)[1];if(null!=p){var d=i.expression.elements.reduce(function(n,e){if(e=e.evaluate(r),0==n.length||1==e.length)n.push(e);else{var i=n[n.length-1];if(2==i.length){var o=""+i[1]+p+e[1];n[n.length-1]=[t(r,o,i[0]),o]}else n.push(e)}return n},[]);if(0==d.length)return e(pt,n,{value:""});if(1==d.length)return d[0][0];if(""==p){var h;return h=d[0][0]instanceof pt||d[1][0]instanceof pt?d.shift()[0]:e(pt,n,{value:""}),d.reduce(function(n,t){return e(Ue,t[0],{operator:"+",left:n,right:t[0]})},h).transform(r)}var _=n.clone();return _.expression=_.expression.clone(),_.expression.expression=_.expression.expression.clone(),_.expression.expression.elements=d.map(function(n){return n[0]}),f(n,_)}}}}return r.option("side_effects")&&n.expression instanceof de&&0==n.args.length&&!J.prototype.has_side_effects.call(n.expression,r)?e(gt,n).transform(r):r.option("drop_console")&&n.expression instanceof qe&&n.expression.expression instanceof st&&"console"==n.expression.expression.name&&n.expression.expression.undeclared()?e(gt,n).transform(r):n.evaluate(r)[0]}),n(Ne,function(n,t){if(t.option("unsafe")){var r=n.expression;if(r instanceof st&&r.undeclared())switch(r.name){case"Object":case"RegExp":case"Function":case"Error":case"Array":return e(Me,n,n).transform(t)}}return n}),n(Re,function(n,t){if(!t.option("side_effects"))return n;if(!n.car.has_side_effects(t)){var r;if(!(n.cdr instanceof st&&"eval"==n.cdr.name&&n.cdr.undeclared()&&(r=t.parent())instanceof Me&&r.expression===n))return n.cdr}if(t.option("cascade")){if(n.car instanceof Le&&!n.car.left.has_side_effects(t)){if(n.car.left.equivalent_to(n.cdr))return n.car;if(n.cdr instanceof Me&&n.cdr.expression.equivalent_to(n.car.left))return n.cdr.expression=n.car,n.cdr}if(!n.car.has_side_effects(t)&&!n.cdr.has_side_effects(t)&&n.car.equivalent_to(n.cdr))return n.car}return n.cdr instanceof je&&"void"==n.cdr.operator&&!n.cdr.expression.has_side_effects(t)?(n.cdr.operator=n.car,n.cdr):n.cdr instanceof gt?e(je,n,{operator:"void",expression:n.car}):n}),Pe.DEFMETHOD("lift_sequences",function(n){if(n.option("sequences")&&this.expression instanceof Re){var e=this.expression,t=e.to_array();return this.expression=t.pop(),t.push(this),e=Re.from_array(t).transform(n)}return this}),n(Ie,function(n,e){return n.lift_sequences(e)}),n(je,function(n,t){n=n.lift_sequences(t);var r=n.expression;if(t.option("booleans")&&t.in_boolean_context()){switch(n.operator){case"!":if(r instanceof je&&"!"==r.operator)return r.expression;break;case"typeof":return t.warn("Boolean expression always true [{file}:{line},{col}]",n.start),e(Et,n)}r instanceof Ue&&"!"==n.operator&&(n=f(n,r.negate(t)))}return n.evaluate(t)[0]}),Ue.DEFMETHOD("lift_sequences",function(n){if(n.option("sequences")){if(this.left instanceof Re){var e=this.left,t=e.to_array();return this.left=t.pop(),t.push(this),e=Re.from_array(t).transform(n)}if(this.right instanceof Re&&this instanceof Le&&!A(this.left,n)){var e=this.right,t=e.to_array();return this.right=t.pop(),t.push(this),e=Re.from_array(t).transform(n)}}return this});var D=g("== === != !== * & | ^");n(Ue,function(n,t){var r=t.has_directive("use asm")?l:function(e,r){if(r||!n.left.has_side_effects(t)&&!n.right.has_side_effects(t)){e&&(n.operator=e);var i=n.left;n.left=n.right,n.right=i}};if(D(n.operator)&&(n.right instanceof lt&&!(n.left instanceof lt)&&(n.left instanceof Ue&&It[n.left.operator]>=It[n.operator]||r(null,!0)),/^[!=]==?$/.test(n.operator))){if(n.left instanceof st&&n.right instanceof Ve){if(n.right.consequent instanceof st&&n.right.consequent.definition()===n.left.definition()){if(/^==/.test(n.operator))return n.right.condition;if(/^!=/.test(n.operator))return n.right.condition.negate(t)}if(n.right.alternative instanceof st&&n.right.alternative.definition()===n.left.definition()){if(/^==/.test(n.operator))return n.right.condition.negate(t);if(/^!=/.test(n.operator))return n.right.condition}}if(n.right instanceof st&&n.left instanceof Ve){if(n.left.consequent instanceof st&&n.left.consequent.definition()===n.right.definition()){if(/^==/.test(n.operator))return n.left.condition;if(/^!=/.test(n.operator))return n.left.condition.negate(t)}if(n.left.alternative instanceof st&&n.left.alternative.definition()===n.right.definition()){if(/^==/.test(n.operator))return n.left.condition.negate(t);if(/^!=/.test(n.operator))return n.left.condition}}}if(n=n.lift_sequences(t),t.option("comparisons"))switch(n.operator){case"===":case"!==":(n.left.is_string(t)&&n.right.is_string(t)||n.left.is_boolean()&&n.right.is_boolean())&&(n.operator=n.operator.substr(0,2));case"==":case"!=":n.left instanceof pt&&"undefined"==n.left.value&&n.right instanceof je&&"typeof"==n.right.operator&&t.option("unsafe")&&(n.right.expression instanceof st&&n.right.expression.undeclared()||(n.right=n.right.expression,n.left=e(gt,n.left).optimize(t),2==n.operator.length&&(n.operator+="=")))}if(t.option("booleans")&&t.in_boolean_context())switch(n.operator){case"&&":var i=n.left.evaluate(t),o=n.right.evaluate(t);if(i.length>1&&!i[1]||o.length>1&&!o[1])return t.warn("Boolean && always false [{file}:{line},{col}]",n.start),e(wt,n);if(i.length>1&&i[1])return o[0];if(o.length>1&&o[1])return i[0];break;case"||":var i=n.left.evaluate(t),o=n.right.evaluate(t);if(i.length>1&&i[1]||o.length>1&&o[1])return t.warn("Boolean || always true [{file}:{line},{col}]",n.start),e(Et,n);if(i.length>1&&!i[1])return o[0];if(o.length>1&&!o[1])return i[0];break;case"+":var i=n.left.evaluate(t),o=n.right.evaluate(t);if(i.length>1&&i[0]instanceof pt&&i[1]||o.length>1&&o[0]instanceof pt&&o[1])return t.warn("+ in boolean context always true [{file}:{line},{col}]",n.start),e(Et,n)}if(t.option("comparisons")){if(!(t.parent()instanceof Ue)||t.parent()instanceof Le){var a=e(je,n,{operator:"!",expression:n.negate(t)});n=f(n,a)}switch(n.operator){case"<":r(">");break;case"<=":r(">=")}}return"+"==n.operator&&n.right instanceof pt&&""===n.right.getValue()&&n.left instanceof Ue&&"+"==n.left.operator&&n.left.is_string(t)?n.left:(t.option("evaluate")&&"+"==n.operator&&(n.left instanceof lt&&n.right instanceof Ue&&"+"==n.right.operator&&n.right.left instanceof lt&&n.right.is_string(t)&&(n=e(Ue,n,{operator:"+",left:e(pt,null,{value:""+n.left.getValue()+n.right.left.getValue(),start:n.left.start,end:n.right.left.end}),right:n.right.right})),n.right instanceof lt&&n.left instanceof Ue&&"+"==n.left.operator&&n.left.right instanceof lt&&n.left.is_string(t)&&(n=e(Ue,n,{operator:"+",left:n.left.left,right:e(pt,null,{value:""+n.left.right.getValue()+n.right.getValue(),start:n.left.right.start,end:n.right.end})})),n.left instanceof Ue&&"+"==n.left.operator&&n.left.is_string(t)&&n.left.right instanceof lt&&n.right instanceof Ue&&"+"==n.right.operator&&n.right.left instanceof lt&&n.right.is_string(t)&&(n=e(Ue,n,{operator:"+",left:e(Ue,n.left,{operator:"+",left:n.left.left,right:e(pt,null,{value:""+n.left.right.getValue()+n.right.left.getValue(),start:n.left.right.start,end:n.right.left.end})}),right:n.right.right}))),n.right instanceof Ue&&n.right.operator==n.operator&&("*"==n.operator||"&&"==n.operator||"||"==n.operator)?(n.left=e(Ue,n.left,{operator:n.operator,left:n.left,right:n.right.left}),n.right=n.right.right,n.transform(t)):n.evaluate(t)[0])}),n(st,function(n,r){if(n.undeclared()){var i=r.option("global_defs");if(i&&i.hasOwnProperty(n.name))return t(r,i[n.name],n);switch(n.name){case"undefined":return e(gt,n);case"NaN":return e(vt,n);case"Infinity":return e(yt,n)}}return n}),n(gt,function(n,t){if(t.option("unsafe")){var r=t.find_parent(ce),i=r.find_variable("undefined");if(i){var o=e(st,n,{name:"undefined",scope:r,thedef:i});return o.reference(),o}}return n});var F=["+","-","/","*","%",">>","<<",">>>","|","^","&"];n(Le,function(n,e){return n=n.lift_sequences(e),"="==n.operator&&n.left instanceof st&&n.right instanceof Ue&&n.right.left instanceof st&&n.right.left.name==n.left.name&&o(n.right.operator,F)&&(n.operator=n.right.operator+"=",n.right=n.right.right),n}),n(Ve,function(n,t){if(!t.option("conditionals"))return n;if(n.condition instanceof Re){var r=n.condition.car;return n.condition=n.condition.cdr,Re.cons(r,n)}var i=n.condition.evaluate(t);if(i.length>1)return i[1]?(t.warn("Condition always true [{file}:{line},{col}]",n.start),n.consequent):(t.warn("Condition always false [{file}:{line},{col}]",n.start),n.alternative);var o=i[0].negate(t);f(i[0],o)===o&&(n=e(Ve,n,{condition:o,consequent:n.alternative,alternative:n.consequent}));var a=n.consequent,u=n.alternative;if(a instanceof Le&&u instanceof Le&&a.operator==u.operator&&a.left.equivalent_to(u.left))return e(Le,n,{operator:a.operator,left:a.left,right:e(Ve,n,{condition:n.condition,consequent:a.right,alternative:u.right})});if(a instanceof Me&&u.TYPE===a.TYPE&&a.args.length==u.args.length&&a.expression.equivalent_to(u.expression)){if(0==a.args.length)return e(Re,n,{car:n.condition,cdr:a});if(1==a.args.length)return a.args[0]=e(Ve,n,{condition:n.condition,consequent:a.args[0],alternative:u.args[0]}),a}return a instanceof Ve&&a.alternative.equivalent_to(u)?e(Ve,n,{condition:e(Ue,n,{left:n.condition,operator:"&&",right:a.condition}),consequent:a.consequent,alternative:u}):n}),n(At,function(n,t){if(t.option("booleans")){var r=t.parent();return r instanceof Ue&&("=="==r.operator||"!="==r.operator)?(t.warn("Non-strict equality against boolean: {operator} {value} [{file}:{line},{col}]",{operator:r.operator,value:n.value,file:r.start.file,line:r.start.line,col:r.start.col}),e(dt,n,{value:+n.value})):e(je,n,{operator:"!",expression:e(dt,n,{value:1-n.value})})}return n}),n(ze,function(n,t){var r=n.property;if(r instanceof pt&&t.option("properties")){if(r=r.getValue(),St(r)?t.option("screw_ie8"):$(r))return e(He,n,{expression:n.expression,property:r}).optimize(t);var i=parseFloat(r);isNaN(i)||i.toString()!=r||(n.property=e(dt,n.property,{value:i}))}return n}),n(He,function(n,e){return n.evaluate(e)[0]}),n(We,w),n(Ye,w),n(ht,w)}(),function(){function n(n){var r="prefix"in n?n.prefix:"UnaryExpression"==n.type?!0:!1;return new(r?je:Ie)({start:e(n),end:t(n),operator:n.operator,expression:i(n.argument)})}function e(n){return new L({file:n.loc&&n.loc.source,line:n.loc&&n.loc.start.line,col:n.loc&&n.loc.start.column,pos:n.start,endpos:n.start})}function t(n){return new L({file:n.loc&&n.loc.source,line:n.loc&&n.loc.end.line,col:n.loc&&n.loc.end.column,pos:n.end,endpos:n.end})}function r(n,r,a){var u="function From_Moz_"+n+"(M){\n";return u+="return new mytype({\nstart: my_start_token(M),\nend: my_end_token(M)",a&&a.split(/\s*,\s*/).forEach(function(n){var e=/([a-z0-9$_]+)(=|@|>|%)([a-z0-9$_]+)/i.exec(n);if(!e)throw new Error("Can't understand property map: "+n);var t="M."+e[1],r=e[2],i=e[3];if(u+=",\n"+i+": ","@"==r)u+=t+".map(from_moz)";else if(">"==r)u+="from_moz("+t+")";else if("="==r)u+=t;else{if("%"!=r)throw new Error("Can't understand operator in propmap: "+n);u+="from_moz("+t+").body"}}),u+="\n})}",u=new Function("mytype","my_start_token","my_end_token","from_moz","return("+u+")")(r,e,t,i),o[n]=u}function i(n){a.push(n);var e=null!=n?o[n.type](n):null;return a.pop(),e}var o={TryStatement:function(n){return new Ce({start:e(n),end:t(n),body:i(n.block).body,bcatch:i(n.handlers?n.handlers[0]:n.handler),bfinally:n.finalizer?new xe(i(n.finalizer)):null})},CatchClause:function(n){return new ke({start:e(n),end:t(n),argname:i(n.param),body:i(n.body).body})},ObjectExpression:function(n){return new Ye({start:e(n),end:t(n),properties:n.properties.map(function(n){var r=n.key,o="Identifier"==r.type?r.name:r.value,a={start:e(r),end:t(n.value),key:o,value:i(n.value)};switch(n.kind){case"init":return new Ge(a);case"set":return a.value.name=i(r),new Ke(a);case"get":return a.value.name=i(r),new Je(a)}})})},SequenceExpression:function(n){return Re.from_array(n.expressions.map(i))},MemberExpression:function(n){return new(n.computed?ze:He)({start:e(n),end:t(n),property:n.computed?i(n.property):n.property.name,expression:i(n.object)})},SwitchCase:function(n){return new(n.test?Se:Fe)({start:e(n),end:t(n),expression:i(n.test),body:n.consequent.map(i)})},Literal:function(n){var r=n.value,i={start:e(n),end:t(n)};if(null===r)return new mt(i);switch(typeof r){case"string":return i.value=r,new pt(i);case"number":return i.value=r,new dt(i);case"boolean":return new(r?Et:wt)(i);default:return i.value=r,new ht(i)}},UnaryExpression:n,UpdateExpression:n,Identifier:function(n){var r=a[a.length-2];return new("this"==n.name?ft:"LabeledStatement"==r.type?ut:"VariableDeclarator"==r.type&&r.id===n?"const"==r.kind?tt:et:"FunctionExpression"==r.type?r.id===n?ot:rt:"FunctionDeclaration"==r.type?r.id===n?it:rt:"CatchClause"==r.type?at:"BreakStatement"==r.type||"ContinueStatement"==r.type?ct:st)({start:e(n),end:t(n),name:n.name})}};r("Node",W),r("Program",fe,"body@body"),r("Function",de,"id>name, params@argnames, body%body"),r("EmptyStatement",Q),r("BlockStatement",Z,"body@body"),r("ExpressionStatement",K,"expression>body"),r("IfStatement",we,"test>condition, consequent>body, alternate>alternative"),r("LabeledStatement",ee,"label>label, body>body"),r("BreakStatement",ye,"label>label"),r("ContinueStatement",Ae,"label>label"),r("WithStatement",se,"object>expression, body>body"),r("SwitchStatement",Ee,"discriminant>expression, cases@body"),r("ReturnStatement",ve,"argument>value"),r("ThrowStatement",ge,"argument>value"),r("WhileStatement",oe,"test>condition, body>body"),r("DoWhileStatement",ie,"test>condition, body>body"),r("ForStatement",ae,"init>init, test>condition, update>step, body>body"),r("ForInStatement",ue,"left>init, right>object, body>body"),r("DebuggerStatement",X),r("FunctionDeclaration",he,"id>name, params@argnames, body%body"),r("VariableDeclaration",Te,"declarations@definitions"),r("VariableDeclarator",Oe,"id>name, init>value"),r("ThisExpression",ft),r("ArrayExpression",We,"elements@elements"),r("FunctionExpression",de,"id>name, params@argnames, body%body"),r("BinaryExpression",Ue,"operator=operator, left>left, right>right"),r("AssignmentExpression",Le,"operator=operator, left>left, right>right"),r("LogicalExpression",Ue,"operator=operator, left>left, right>right"),r("ConditionalExpression",Ve,"test>condition, consequent>consequent, alternate>alternative"),r("NewExpression",Ne,"callee>expression, arguments@args"),r("CallExpression",Me,"callee>expression, arguments@args");var a=null;W.from_mozilla_ast=function(n){var e=a;a=[];var t=i(n);return a=e,t}}(),n.array_to_hash=t,n.slice=r,n.characters=i,n.member=o,n.find_if=a,n.repeat_string=u,n.DefaultsError=s,n.defaults=c,n.merge=f,n.noop=l,n.MAP=V,n.push_uniq=p,n.string_template=d,n.remove=h,n.mergeSort=_,n.set_difference=m,n.set_intersection=v,n.makePredicate=g,n.all=b,n.Dictionary=y,n.DEFNODE=A,n.AST_Token=L,n.AST_Node=W,n.AST_Statement=Y,n.AST_Debugger=X,n.AST_Directive=G,n.AST_SimpleStatement=K,n.walk_body=w,n.AST_Block=J,n.AST_BlockStatement=Z,n.AST_EmptyStatement=Q,n.AST_StatementWithBody=ne,n.AST_LabeledStatement=ee,n.AST_IterationStatement=te,n.AST_DWLoop=re,n.AST_Do=ie,n.AST_While=oe,n.AST_For=ae,n.AST_ForIn=ue,n.AST_With=se,n.AST_Scope=ce,n.AST_Toplevel=fe,n.AST_Lambda=le,n.AST_Accessor=pe,n.AST_Function=de,n.AST_Defun=he,n.AST_Jump=_e,n.AST_Exit=me,n.AST_Return=ve,n.AST_Throw=ge,n.AST_LoopControl=be,n.AST_Break=ye,n.AST_Continue=Ae,n.AST_If=we,n.AST_Switch=Ee,n.AST_SwitchBranch=De,n.AST_Default=Fe,n.AST_Case=Se,n.AST_Try=Ce,n.AST_Catch=ke,n.AST_Finally=xe,n.AST_Definitions=Be,n.AST_Var=Te,n.AST_Const=$e,n.AST_VarDef=Oe,n.AST_Call=Me,n.AST_New=Ne,n.AST_Seq=Re,n.AST_PropAccess=qe,n.AST_Dot=He,n.AST_Sub=ze,n.AST_Unary=Pe,n.AST_UnaryPrefix=je,n.AST_UnaryPostfix=Ie,n.AST_Binary=Ue,n.AST_Conditional=Ve,n.AST_Assign=Le,n.AST_Array=We,n.AST_Object=Ye,n.AST_ObjectProperty=Xe,n.AST_ObjectKeyVal=Ge,n.AST_ObjectSetter=Ke,n.AST_ObjectGetter=Je,n.AST_Symbol=Ze,n.AST_SymbolAccessor=Qe,n.AST_SymbolDeclaration=nt,n.AST_SymbolVar=et,n.AST_SymbolConst=tt,n.AST_SymbolFunarg=rt,n.AST_SymbolDefun=it,n.AST_SymbolLambda=ot,n.AST_SymbolCatch=at,n.AST_Label=ut,n.AST_SymbolRef=st,n.AST_LabelRef=ct,n.AST_This=ft,n.AST_Constant=lt,n.AST_String=pt,n.AST_Number=dt,n.AST_RegExp=ht,n.AST_Atom=_t,n.AST_Null=mt,n.AST_NaN=vt,n.AST_Undefined=gt,n.AST_Hole=bt,n.AST_Infinity=yt,n.AST_Boolean=At,n.AST_False=wt,n.AST_True=Et,n.TreeWalker=E,n.KEYWORDS=Dt,n.KEYWORDS_ATOM=Ft,n.RESERVED_WORDS=St,n.KEYWORDS_BEFORE_EXPRESSION=Ct,n.OPERATOR_CHARS=kt,n.RE_HEX_NUMBER=xt,n.RE_OCT_NUMBER=Bt,n.RE_DEC_NUMBER=Tt,n.OPERATORS=$t,n.WHITESPACE_CHARS=Ot,n.PUNC_BEFORE_EXPRESSION=Mt,n.PUNC_CHARS=Nt,n.REGEXP_MODIFIERS=Rt,n.UNICODE=qt,n.is_letter=D,n.is_digit=F,n.is_alphanumeric_char=S,n.is_unicode_combining_mark=C,n.is_unicode_connector_punctuation=k,n.is_identifier=x,n.is_identifier_start=B,n.is_identifier_char=T,n.is_identifier_string=$,n.parse_js_number=O,n.JS_Parse_Error=M,n.js_error=N,n.is_token=R,n.EX_EOF=Ht,n.tokenizer=q,n.UNARY_PREFIX=zt,n.UNARY_POSTFIX=Pt,n.ASSIGNMENT=jt,n.PRECEDENCE=It,n.STATEMENTS_WITH_LABELS=Ut,n.ATOMIC_START_TOKEN=Vt,n.parse=H,n.TreeTransformer=z,n.SymbolDef=P,n.base54=Lt,n.OutputStream=j,n.Compressor=I,n.SourceMap=U}({},function(){return this}());
================================================
FILE: misc/demo/index.html
================================================
Angular directives for Bootstrap
Angular 2
For Angular 2 support, check out
ng-bootstrap
, created by the UI Bootstrap team.
Dependencies
This repository contains a set of native AngularJS directives based on
Bootstrap's markup and CSS. As a result no dependency on jQuery or Bootstrap's
JavaScript is required. The only required dependencies are:
- AngularJS (requires AngularJS 1.4.x or higher, tested with <%= ngversion %>).
0.14.3 is the last version of this library that supports AngularJS 1.3.x and
0.12.0 is the last version that supports AngularJS 1.2.x.
- Angular-animate (the version should match with your angular's, tested with <%= ngversion %>) if you plan in using animations, you need to load angular-animate as well.
- Angular-touch (the version should match with your angular's, tested with <%= ngversion %>) if you plan in using swipe actions, you need to load angular-touch as well.
- Bootstrap CSS (tested with version <%= bsversion %>).
This version of the library (<%= pkg.version%>) works only with Bootstrap CSS in version 3.x.
0.8.0 is the last version of this library that supports Bootstrap CSS in version 2.3.x.
Files to download
Build files for all directives are distributed in several flavours: minified for production usage, un-minified
for development, with or without templates. All the options are described and can be
downloaded from here. It should be noted that the -tpls files contain the templates bundled in JavaScript, while the regular version does not contain the bundled templates. For more information, check out the FAQ here and the README here.
Alternativelly, if you are only interested in a subset of directives, you can
create your own build.
Whichever method you choose the good news that the overall size of a download is fairly small:
122K minified for all directives with templates and 98K without (~31kB with gzip
compression, with templates, and 28K gzipped without)
Installation
As soon as you've got all the files downloaded and included in your page you just need to declare
a dependency on the ui.bootstrap module:
angular.module('myModule', ['ui.bootstrap']);
If you are using UI Bootstrap in the CSP mode, e.g. in an extension, make sure you link to the ui-bootstrap-csp.css in your HTML manually.
You can fork one of the plunkers from this page to see a working example of what is described here.
Migration to prefixes
Since version 0.14.0 we started to prefix all our components. If you are upgrading from ui-bootstrap 0.13.4 or earlier,
check our migration guide.
CSS
Original Bootstrap's CSS depends on empty href attributes to style cursors for several components (pagination, tabs etc.).
But in AngularJS adding empty href attributes to link tags will cause unwanted route changes. This is why we need to remove empty href attributes from directive templates and as a result styling is not applied correctly. The remedy is simple, just add the following styling to your application:
.nav, .pagination, .carousel, .panel-title a { cursor: pointer; }
FAQ
Please check our FAQ section for common problems / solutions.
Reading the documentation
Each of the components provided in ui-bootstrap have documentation and interactive Plunker examples.
For the directives, we list the different attributes with their default values. In addition to this, some settings have a badge on it:
- - This setting has an angular $watch listener applied to it.
- B - This setting is a boolean. It doesn't need a parameter.
- C - This setting can be configured globally in a constant service*.
- $ - This setting expects an angular expression instead of a literal string. If the expression support a boolean / integer, you can pass it directly.
- readonly - This setting is readonly.
For the services (you will recognize them with the $ prefix), we list all the possible parameters you can pass to them and their default values if any.
* Some directives have a config service that follows the next pattern: uibDirectiveConfig. The service's settings use camel case. The services can be configured in a .config function for example.
<% demoModules.forEach(function(module) { %>
<%= module.docs.html %>
<%= module.docs.md %>
<% }); %>
================================================
FILE: misc/raw-files-generator.js
================================================
/*!
* Forked from:
* Bootstrap Grunt task for generating raw-files.min.js for the Customizer
* http://getbootstrap.com
* Copyright 2014 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
*/
/* jshint node: true */
'use strict';
var fs = require('fs');
function getFiles(filePaths) {
var files = {};
filePaths
.forEach(function (path) {
files[path] = fs.readFileSync(path, 'utf8');
});
return files;
}
module.exports = function generateRawFilesJs(grunt, jsFilename, files, banner, cssBanner) {
if (!banner) {
banner = '';
}
if (!cssBanner) {
cssBanner = '';
}
var filesJsObject = {
banner: banner,
cssBanner: cssBanner,
files: getFiles(files),
};
var filesJsContent = JSON.stringify(filesJsObject);
try {
fs.writeFileSync(jsFilename, filesJsContent);
}
catch (err) {
grunt.fail.warn(err);
}
grunt.log.writeln('File ' + jsFilename.cyan + ' created.');
};
================================================
FILE: misc/test-lib/helpers.js
================================================
// jasmine matcher for expecting an element to have a css class
// https://github.com/angular/angular.js/blob/master/test/matchers.js
beforeEach(function() {
jasmine.addMatchers({
toHaveClass: function(util, customEqualityTesters) {
return {
compare: function(actual, expected) {
var result = {
pass: actual.hasClass(expected)
};
if (result.pass) {
result.message = 'Expected "' + actual + '" not to have the "' + expected + '" class.';
} else {
result.message = 'Expected "' + actual + '" to have the "' + expected + '" class.';
}
return result;
}
}
},
toBeHidden: function(util, customEqualityTesters) {
return {
compare: function(actual) {
var result = {
pass: actual.hasClass('ng-hide') || actual.css('display') === 'none'
};
if (result.pass) {
result.message = 'Expected "' + actual + '" not to be hidden';
} else {
result.message = 'Expected "' + actual + '" to be hidden';
}
return result;
}
}
},
toHaveFocus: function(util, customEqualityTesters) {
return {
compare: function(actual) {
var result = {
pass: document.activeElement === actual[0]
};
if (result.pass) {
result.message = 'Expected "' + actual + '" not to have focus';
} else {
result.message = 'Expected "' + actual + '" to have focus';
}
return result;
}
}
}
});
});
================================================
FILE: misc/validate-commit-msg.js
================================================
#!/usr/bin/env node
/**
* Git COMMIT-MSG hook for validating commit message
* See https://docs.google.com/document/d/1rk04jEuGfk9kYzfqCuOlPTSJw3hEDZJTBN5E5f1SALo/edit
*
* Installation:
* >> cd
* >> ln -s validate-commit-msg.js .git/hooks/commit-msg
*/
var fs = require('fs');
var util = require('util');
var MAX_LENGTH = 70;
var PATTERN = /^(?:fixup!\s*)?(\w*)(\((\w+)\))?\: (.*)$/;
var IGNORED = /^WIP\:/;
var TYPES = {
chore: true,
demo: true,
docs: true,
feat: true,
fix: true,
refactor: true,
revert: true,
style: true,
test: true
};
var error = function() {
// gitx does not display it
// http://gitx.lighthouseapp.com/projects/17830/tickets/294-feature-display-hook-error-message-when-hook-fails
// https://groups.google.com/group/gitx/browse_thread/thread/a03bcab60844b812
console.error('INVALID COMMIT MSG: ' + util.format.apply(null, arguments));
};
var validateMessage = function(message) {
var isValid = true;
if (IGNORED.test(message)) {
console.log('Commit message validation ignored.');
return true;
}
if (message.length > MAX_LENGTH) {
error('is longer than %d characters !', MAX_LENGTH);
isValid = false;
}
var match = PATTERN.exec(message);
if (!match) {
error('does not match "(): " ! was: "' + message + '"\nNote: must be only letters.');
return false;
}
var type = match[1];
var scope = match[3];
var subject = match[4];
if (!TYPES.hasOwnProperty(type)) {
error('"%s" is not allowed type !', type);
return false;
}
// Some more ideas, do want anything like this ?
// - allow only specific scopes (eg. fix(docs) should not be allowed ?
// - auto correct the type to lower case ?
// - auto correct first letter of the subject to lower case ?
// - auto add empty line after subject ?
// - auto remove empty () ?
// - auto correct typos in type ?
// - store incorrect messages, so that we can learn
return isValid;
};
var firstLineFromBuffer = function(buffer) {
return buffer.toString().split('\n').shift();
};
// publish for testing
exports.validateMessage = validateMessage;
// hacky start if not run by jasmine :-D
if (process.argv.join('').indexOf('jasmine-node') === -1) {
var commitMsgFile = process.argv[2];
var incorrectLogFile = commitMsgFile.replace('COMMIT_EDITMSG', 'logs/incorrect-commit-msgs');
fs.readFile(commitMsgFile, function(err, buffer) {
var msg = firstLineFromBuffer(buffer);
if (!validateMessage(msg)) {
fs.appendFile(incorrectLogFile, msg + '\n', function() {
process.exit(1);
});
} else {
process.exit(0);
}
});
}
================================================
FILE: package.json
================================================
{
"author": "https://github.com/angular-ui/bootstrap/graphs/contributors",
"name": "angular-ui-bootstrap",
"version": "2.5.4",
"description": "Native AngularJS (Angular) directives for Bootstrap",
"homepage": "http://angular-ui.github.io/bootstrap/",
"keywords": [
"angularjs",
"angular",
"bootstrap",
"ui"
],
"dependencies": {},
"directories": {
"lib": "src/"
},
"files": [
"index.js",
"dist/",
"src/",
"template/"
],
"main": "index.js",
"scripts": {
"demo": "grunt after-test && static dist -a 0.0.0.0 -H '{\"Cache-Control\": \"no-cache, must-revalidate\"}'",
"test": "grunt"
},
"repository": {
"type": "git",
"url": "https://github.com/angular-ui/bootstrap.git"
},
"devDependencies": {
"angular": "1.6.1",
"angular-mocks": "1.6.1",
"angular-sanitize": "1.6.1",
"grunt": "^0.4.5",
"grunt-cli": "^1.2.0",
"grunt-contrib-concat": "^1.0.0",
"grunt-contrib-copy": "^1.0.0",
"grunt-contrib-uglify": "^1.0.1",
"grunt-contrib-watch": "^1.0.0",
"grunt-conventional-changelog": "^6.1.0",
"grunt-ddescribe-iit": "0.0.6",
"grunt-eslint": "^17.3.1",
"grunt-html2js": "^0.3.0",
"grunt-karma": "^0.12.0",
"jasmine-core": "^2.2.0",
"karma": "0.13.22",
"karma-chrome-launcher": "^0.2.0",
"karma-coverage": "^0.5.0",
"karma-firefox-launcher": "^0.1.4",
"karma-jasmine": "^0.3.5",
"load-grunt-tasks": "^3.3.0",
"lodash": "^4.1.0",
"marked": "^0.3.5",
"node-static": "^0.7.8",
"semver": "^5.0.1",
"shelljs": "^0.6.0"
},
"license": "MIT"
}
================================================
FILE: src/accordion/accordion.js
================================================
angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse', 'ui.bootstrap.tabindex'])
.constant('uibAccordionConfig', {
closeOthers: true
})
.controller('UibAccordionController', ['$scope', '$attrs', 'uibAccordionConfig', function($scope, $attrs, accordionConfig) {
// This array keeps track of the accordion groups
this.groups = [];
// Ensure that all the groups in this accordion are closed, unless close-others explicitly says not to
this.closeOthers = function(openGroup) {
var closeOthers = angular.isDefined($attrs.closeOthers) ?
$scope.$eval($attrs.closeOthers) : accordionConfig.closeOthers;
if (closeOthers) {
angular.forEach(this.groups, function(group) {
if (group !== openGroup) {
group.isOpen = false;
}
});
}
};
// This is called from the accordion-group directive to add itself to the accordion
this.addGroup = function(groupScope) {
var that = this;
this.groups.push(groupScope);
groupScope.$on('$destroy', function(event) {
that.removeGroup(groupScope);
});
};
// This is called from the accordion-group directive when to remove itself
this.removeGroup = function(group) {
var index = this.groups.indexOf(group);
if (index !== -1) {
this.groups.splice(index, 1);
}
};
}])
// The accordion directive simply sets up the directive controller
// and adds an accordion CSS class to itself element.
.directive('uibAccordion', function() {
return {
controller: 'UibAccordionController',
controllerAs: 'accordion',
transclude: true,
templateUrl: function(element, attrs) {
return attrs.templateUrl || 'uib/template/accordion/accordion.html';
}
};
})
// The accordion-group directive indicates a block of html that will expand and collapse in an accordion
.directive('uibAccordionGroup', function() {
return {
require: '^uibAccordion', // We need this directive to be inside an accordion
transclude: true, // It transcludes the contents of the directive into the template
restrict: 'A',
templateUrl: function(element, attrs) {
return attrs.templateUrl || 'uib/template/accordion/accordion-group.html';
},
scope: {
heading: '@', // Interpolate the heading attribute onto this scope
panelClass: '@?', // Ditto with panelClass
isOpen: '=?',
isDisabled: '=?'
},
controller: function() {
this.setHeading = function(element) {
this.heading = element;
};
},
link: function(scope, element, attrs, accordionCtrl) {
element.addClass('panel');
accordionCtrl.addGroup(scope);
scope.openClass = attrs.openClass || 'panel-open';
scope.panelClass = attrs.panelClass || 'panel-default';
scope.$watch('isOpen', function(value) {
element.toggleClass(scope.openClass, !!value);
if (value) {
accordionCtrl.closeOthers(scope);
}
});
scope.toggleOpen = function($event) {
if (!scope.isDisabled) {
if (!$event || $event.which === 32) {
scope.isOpen = !scope.isOpen;
}
}
};
var id = 'accordiongroup-' + scope.$id + '-' + Math.floor(Math.random() * 10000);
scope.headingId = id + '-tab';
scope.panelId = id + '-panel';
}
};
})
// Use accordion-heading below an accordion-group to provide a heading containing HTML
.directive('uibAccordionHeading', function() {
return {
transclude: true, // Grab the contents to be used as the heading
template: '', // In effect remove this element!
replace: true,
require: '^uibAccordionGroup',
link: function(scope, element, attrs, accordionGroupCtrl, transclude) {
// Pass the heading to the accordion-group controller
// so that it can be transcluded into the right place in the template
// [The second parameter to transclude causes the elements to be cloned so that they work in ng-repeat]
accordionGroupCtrl.setHeading(transclude(scope, angular.noop));
}
};
})
// Use in the accordion-group template to indicate where you want the heading to be transcluded
// You must provide the property on the accordion-group controller that will hold the transcluded element
.directive('uibAccordionTransclude', function() {
return {
require: '^uibAccordionGroup',
link: function(scope, element, attrs, controller) {
scope.$watch(function() { return controller[attrs.uibAccordionTransclude]; }, function(heading) {
if (heading) {
var elem = angular.element(element[0].querySelector(getHeaderSelectors()));
elem.html('');
elem.append(heading);
}
});
}
};
function getHeaderSelectors() {
return 'uib-accordion-header,' +
'data-uib-accordion-header,' +
'x-uib-accordion-header,' +
'uib\\:accordion-header,' +
'[uib-accordion-header],' +
'[data-uib-accordion-header],' +
'[x-uib-accordion-header]';
}
});
================================================
FILE: src/accordion/docs/demo.html
================================================
This content is straight in the template.
{{group.content}}
The body of the uib-accordion group grows to fit the contents
{{item}}
Hello
Custom template with custom header template
World
Please, to delete your account, click the button below
I can have markup, too!
This is just some content to illustrate fancy headings.
================================================
FILE: src/accordion/docs/demo.js
================================================
angular.module('ui.bootstrap.demo').controller('AccordionDemoCtrl', function ($scope) {
$scope.oneAtATime = true;
$scope.groups = [
{
title: 'Dynamic Group Header - 1',
content: 'Dynamic Group Body - 1'
},
{
title: 'Dynamic Group Header - 2',
content: 'Dynamic Group Body - 2'
}
];
$scope.items = ['Item 1', 'Item 2', 'Item 3'];
$scope.addItem = function() {
var newItemNo = $scope.items.length + 1;
$scope.items.push('Item ' + newItemNo);
};
$scope.status = {
isCustomHeaderOpen: false,
isFirstOpen: true,
isFirstDisabled: false
};
});
================================================
FILE: src/accordion/docs/readme.md
================================================
The **accordion directive** builds on top of the collapse directive to provide a list of items, with collapsible bodies that are collapsed or expanded by clicking on the item's header.
The body of each accordion group is transcluded into the body of the collapsible element.
### uib-accordion settings
* `close-others`
$
C
_(Default: `true`)_ -
Control whether expanding an item will cause the other items to close.
* `template-url`
_(Default: `template/accordion/accordion.html`)_ -
Add the ability to override the template used on the component.
### uib-accordion-group settings
* `heading`
_(Default: `none`)_ -
The clickable text on the group's header. You need one to be able to click on the header for toggling.
* `is-disabled`
$
_(Default: `false`)_ -
Whether the accordion group is disabled or not.
* `is-open`
$
_(Default: `false`)_ -
Whether accordion group is open or closed.
* `template-url`
_(Default: `uib/template/accordion/accordion-group.html`)_ -
Add the ability to override the template used on the component.
### Accordion heading
Instead of the `heading` attribute on the `uib-accordion-group`, you can use an `uib-accordion-heading` element inside a group that will be used as the group's header.
If you're using a custom template for the `uib-accordion-group`, you'll need to have an element for the heading to be transcluded into using `uib-accordion-header` (e.g. ``).
### Known issues
To use clickable elements within the accordion, you have to override the accordion-group template to use div elements instead of anchor elements, and add `cursor: pointer` in your CSS. This is due to browsers interpreting anchor elements as the target of any click event, which triggers routing when certain elements such as buttons are nested inside the anchor element.
If custom classes on the accordion-group element are desired, one needs to either modify the template to remove the `ng-class` usage in the accordion-group template and use ng-class on the accordion-group element (not recommended), or use an interpolated expression in the class attribute, i.e. ``.
================================================
FILE: src/accordion/index.js
================================================
require('../collapse');
require('../tabindex');
require('../../template/accordion/accordion-group.html.js');
require('../../template/accordion/accordion.html.js');
require('./accordion');
var MODULE_NAME = 'ui.bootstrap.module.accordion';
angular.module(MODULE_NAME, ['ui.bootstrap.accordion', 'uib/template/accordion/accordion.html', 'uib/template/accordion/accordion-group.html']);
module.exports = MODULE_NAME;
================================================
FILE: src/accordion/test/accordion.spec.js
================================================
describe('uib-accordion', function() {
var $animate, $scope;
beforeEach(module('ui.bootstrap.accordion'));
beforeEach(module('ngAnimateMock'));
beforeEach(module('uib/template/accordion/accordion.html'));
beforeEach(module('uib/template/accordion/accordion-group.html'));
beforeEach(inject(function(_$animate_, $rootScope) {
$animate = _$animate_;
$scope = $rootScope;
}));
describe('controller', function () {
var ctrl, $element, $attrs;
beforeEach(inject(function($controller) {
$attrs = {};
ctrl = $controller('UibAccordionController', { $scope: $scope, $attrs: $attrs });
}));
describe('addGroup', function() {
it('adds a the specified panel to the collection', function() {
var group1, group2;
ctrl.addGroup(group1 = $scope.$new());
ctrl.addGroup(group2 = $scope.$new());
expect(ctrl.groups.length).toBe(2);
expect(ctrl.groups[0]).toBe(group1);
expect(ctrl.groups[1]).toBe(group2);
});
});
describe('closeOthers', function() {
var group1, group2, group3;
beforeEach(function() {
ctrl.addGroup(group1 = { isOpen: true, $on : angular.noop });
ctrl.addGroup(group2 = { isOpen: true, $on : angular.noop });
ctrl.addGroup(group3 = { isOpen: true, $on : angular.noop });
});
it('should close other panels if close-others attribute is not defined', function() {
delete $attrs.closeOthers;
ctrl.closeOthers(group2);
expect(group1.isOpen).toBe(false);
expect(group2.isOpen).toBe(true);
expect(group3.isOpen).toBe(false);
});
it('should close other panels if close-others attribute is true', function() {
$attrs.closeOthers = 'true';
ctrl.closeOthers(group3);
expect(group1.isOpen).toBe(false);
expect(group2.isOpen).toBe(false);
expect(group3.isOpen).toBe(true);
});
it('should not close other panels if close-others attribute is false', function() {
$attrs.closeOthers = 'false';
ctrl.closeOthers(group2);
expect(group1.isOpen).toBe(true);
expect(group2.isOpen).toBe(true);
expect(group3.isOpen).toBe(true);
});
describe('setting accordionConfig', function() {
var originalCloseOthers;
beforeEach(inject(function(uibAccordionConfig) {
originalCloseOthers = uibAccordionConfig.closeOthers;
uibAccordionConfig.closeOthers = false;
}));
afterEach(inject(function(uibAccordionConfig) {
// return it to the original value
uibAccordionConfig.closeOthers = originalCloseOthers;
}));
it('should not close other panels if accordionConfig.closeOthers is false', function() {
ctrl.closeOthers(group2);
expect(group1.isOpen).toBe(true);
expect(group2.isOpen).toBe(true);
expect(group3.isOpen).toBe(true);
});
});
});
describe('removeGroup', function() {
it('should remove the specified panel', function() {
var group1, group2, group3;
ctrl.addGroup(group1 = $scope.$new());
ctrl.addGroup(group2 = $scope.$new());
ctrl.addGroup(group3 = $scope.$new());
ctrl.removeGroup(group2);
expect(ctrl.groups.length).toBe(2);
expect(ctrl.groups[0]).toBe(group1);
expect(ctrl.groups[1]).toBe(group3);
});
it('should ignore remove of non-existing panel', function() {
var group1, group2;
ctrl.addGroup(group1 = $scope.$new());
ctrl.addGroup(group2 = $scope.$new());
expect(ctrl.groups.length).toBe(2);
ctrl.removeGroup({});
expect(ctrl.groups.length).toBe(2);
});
it('should remove a panel when the scope is destroyed', function() {
var group1, group2, group3;
ctrl.addGroup(group1 = $scope.$new());
ctrl.addGroup(group2 = $scope.$new());
ctrl.addGroup(group3 = $scope.$new());
group2.$destroy();
expect(ctrl.groups.length).toBe(2);
expect(ctrl.groups[0]).toBe(group1);
expect(ctrl.groups[1]).toBe(group3);
});
});
});
describe('uib-accordion', function() {
var scope, $compile, $templateCache, element;
beforeEach(inject(function($rootScope, _$compile_, _$templateCache_) {
scope = $rootScope;
$compile = _$compile_;
$templateCache = _$templateCache_;
}));
it('should be a tablist', function() {
element = $compile('')(scope);
scope.$digest();
expect(element.html()).toContain('role="tablist"');
});
it('should expose the controller on the view', function() {
$templateCache.put('uib/template/accordion/accordion.html', '{{accordion.text}}
');
element = $compile('')(scope);
scope.$digest();
var ctrl = element.controller('uibAccordion');
expect(ctrl).toBeDefined();
ctrl.text = 'foo';
scope.$digest();
expect(element.html()).toBe('foo
');
});
it('should allow custom templates', function() {
$templateCache.put('foo/bar.html', 'baz
');
element = $compile('')(scope);
scope.$digest();
expect(element.html()).toBe('baz
');
});
});
describe('uib-accordion-group', function() {
var scope, $compile;
var element, groups;
var findGroupHeading = function(index) {
return groups.eq(index).find('.panel-heading').eq(0);
};
var findGroupLink = function(index) {
return groups.eq(index).find('.accordion-toggle').eq(0);
};
var findGroupBody = function(index) {
return groups.eq(index).find('.panel-collapse').eq(0);
};
beforeEach(inject(function(_$rootScope_, _$compile_) {
scope = _$rootScope_;
$compile = _$compile_;
}));
it('should allow custom templates', inject(function($templateCache) {
$templateCache.put('foo/bar.html', 'baz
');
var tpl =
'' +
'' +
'';
element = $compile(tpl)(scope);
scope.$digest();
expect(element.find('[template-url]').html()).toBe('baz
');
}));
describe('with static panels', function() {
beforeEach(function() {
spyOn(Math, 'random').and.returnValue(0.1);
var tpl =
'' +
'Content 1
' +
'Content 2
' +
'';
element = angular.element(tpl);
$compile(element)(scope);
scope.$digest();
groups = element.find('.panel');
});
afterEach(function() {
element.remove();
});
it('should create accordion panels with content', function() {
expect(groups.length).toEqual(2);
expect(findGroupLink(0).text()).toEqual('title 1');
expect(findGroupBody(0).text().trim()).toEqual('Content 1');
expect(findGroupLink(1).text()).toEqual('title 2');
expect(findGroupBody(1).text().trim()).toEqual('Content 2');
});
it('should change selected element on click', function() {
findGroupLink(0).click();
scope.$digest();
expect(findGroupBody(0).scope().isOpen).toBe(true);
expect(findGroupHeading(0).html()).toContain('aria-expanded="true"');
findGroupLink(1).click();
scope.$digest();
expect(findGroupBody(0).scope().isOpen).toBe(false);
expect(findGroupHeading(0).html()).toContain('aria-expanded="false"');
expect(findGroupBody(1).scope().isOpen).toBe(true);
expect(findGroupHeading(1).html()).toContain('aria-expanded="true"');
});
it('should toggle element on click', function() {
findGroupLink(0).click();
scope.$digest();
expect(findGroupBody(0).scope().isOpen).toBe(true);
expect(groups.eq(0).html()).toContain('aria-hidden="false"');
findGroupLink(0).click();
scope.$digest();
expect(findGroupBody(0).scope().isOpen).toBe(false);
expect(groups.eq(0).html()).toContain('aria-hidden="true"');
});
it('should add, by default, "panel-open" when opened', function() {
var group = groups.eq(0);
findGroupLink(0).click();
scope.$digest();
expect(group).toHaveClass('panel-open');
findGroupLink(0).click();
scope.$digest();
expect(group).not.toHaveClass('panel-open');
});
it('should toggle element on spacebar when focused', function() {
var group = groups.eq(0);
findGroupLink(0)[0].focus();
var e = $.Event('keypress');
e.which = 32;
findGroupLink(0).trigger(e);
expect(group).toHaveClass('panel-open');
e = $.Event('keypress');
e.which = 32;
findGroupLink(0).trigger(e);
expect(group).not.toHaveClass('panel-open');
});
it('should not toggle with any other keyCode', function() {
var group = groups.eq(0);
findGroupLink(0)[0].focus();
var e = $.Event('keypress');
e.which = 65;
findGroupLink(0).trigger(e);
expect(group).not.toHaveClass('panel-open');
});
it('should generate an Id for the heading', function() {
var groupScope = findGroupBody(0).scope();
expect(groupScope.headingId).toEqual('accordiongroup-' + groupScope.$id + '-1000-tab');
});
it('should generate an Id for the panel', function() {
var groupScope = findGroupBody(0).scope();
expect(groupScope.panelId).toEqual('accordiongroup-' + groupScope.$id + '-1000-panel');
});
});
describe('with open-class attribute', function() {
beforeEach(function() {
var tpl =
'' +
'Content 1
' +
'Content 2
' +
'';
element = angular.element(tpl);
$compile(element)(scope);
scope.$digest();
groups = element.find('.panel');
});
afterEach(function() {
element.remove();
});
it('should add custom-open-class when opened', function() {
var group = groups.eq(0);
findGroupLink(0).click();
scope.$digest();
expect(group).toHaveClass('custom-open-class');
findGroupLink(0).click();
scope.$digest();
expect(group).not.toHaveClass('custom-open-class');
});
});
describe('with dynamic panels', function() {
var model;
beforeEach(function() {
var tpl =
'' +
'{{group.content}}
' +
'';
element = angular.element(tpl);
model = [
{name: 'title 1', content: 'Content 1'},
{name: 'title 2', content: 'Content 2'}
];
$compile(element)(scope);
scope.$digest();
});
it('should have no panels initially', function() {
groups = element.find('.panel');
expect(groups.length).toEqual(0);
});
it('should have a panel for each model item', function() {
scope.groups = model;
scope.$digest();
groups = element.find('.panel');
expect(groups.length).toEqual(2);
expect(findGroupLink(0).text()).toEqual('title 1');
expect(findGroupBody(0).text().trim()).toEqual('Content 1');
expect(findGroupLink(1).text()).toEqual('title 2');
expect(findGroupBody(1).text().trim()).toEqual('Content 2');
});
it('should react properly on removing items from the model', function() {
scope.groups = model;
scope.$digest();
groups = element.find('.panel');
expect(groups.length).toEqual(2);
scope.groups.splice(0,1);
scope.$digest();
groups = element.find('.panel');
expect(groups.length).toEqual(1);
});
});
describe('is-open attribute', function() {
beforeEach(function() {
var tpl =
'' +
'Content 1
' +
'Content 2
' +
'';
element = angular.element(tpl);
scope.open = { first: false, second: true };
$compile(element)(scope);
scope.$digest();
groups = element.find('.panel');
});
it('should open the panel with isOpen set to true', function() {
expect(findGroupBody(0).scope().isOpen).toBe(false);
expect(findGroupBody(1).scope().isOpen).toBe(true);
});
it('should toggle variable on element click', function() {
findGroupLink(0).click();
scope.$digest();
expect(scope.open.first).toBe(true);
findGroupLink(0).click();
scope.$digest();
expect(scope.open.second).toBe(false);
});
});
describe('is-open attribute with dynamic content', function() {
beforeEach(function() {
var tpl =
'' +
'' +
'Static content
' +
'';
element = angular.element(tpl);
scope.items = ['Item 1', 'Item 2', 'Item 3'];
scope.open1 = true;
scope.open2 = false;
angular.element(document.body).append(element);
$compile(element)(scope);
scope.$digest();
$animate.flush();
groups = element.find('.panel');
});
afterEach(function() {
element.remove();
});
it('should have visible panel body when the group with isOpen set to true', function() {
expect(findGroupBody(0)).toHaveClass('in');
expect(findGroupBody(1)).not.toHaveClass('in');
});
});
describe('is-open attribute with dynamic groups', function() {
beforeEach(function() {
var tpl =
'' +
'{{group.content}}
' +
'';
element = angular.element(tpl);
scope.groups = [
{name: 'title 1', content: 'Content 1', open: false},
{name: 'title 2', content: 'Content 2', open: true}
];
$compile(element)(scope);
scope.$digest();
groups = element.find('.panel');
});
it('should have visible group body when the group with isOpen set to true', function() {
expect(findGroupBody(0).scope().isOpen).toBe(false);
expect(findGroupBody(1).scope().isOpen).toBe(true);
});
it('should toggle element on click', function() {
findGroupLink(0).click();
scope.$digest();
expect(findGroupBody(0).scope().isOpen).toBe(true);
expect(scope.groups[0].open).toBe(true);
findGroupLink(0).click();
scope.$digest();
expect(findGroupBody(0).scope().isOpen).toBe(false);
expect(scope.groups[0].open).toBe(false);
});
});
describe('is-open attribute with custom class', function() {
beforeEach(function() {
var tpl =
'' +
'{{group.content}}
' +
'';
element = angular.element(tpl);
scope.groups = [
{name: 'title 1', content: 'Content 1', open: false},
{name: 'title 2', content: 'Content 2', open: true}
];
$compile(element)(scope);
scope.$digest();
groups = element.find('.panel');
});
it('should add "panel-open" class', function(){
expect(groups.eq(0)).not.toHaveClass('panel-open');
expect(groups.eq(1)).toHaveClass('panel-open');
});
});
describe('`is-disabled` attribute', function() {
var groupBody;
beforeEach(function() {
var tpl =
'' +
'Content 1
' +
'';
element = angular.element(tpl);
scope.disabled = true;
$compile(element)(scope);
scope.$digest();
groups = element.find('.panel');
groupBody = findGroupBody(0);
});
it('should open the panel with isOpen set to true', function() {
expect(groupBody.scope().isOpen).toBeFalsy();
});
it('should not toggle if disabled', function() {
findGroupLink(0).click();
scope.$digest();
expect(groupBody.scope().isOpen).toBeFalsy();
});
it('should toggle after enabling', function() {
scope.disabled = false;
scope.$digest();
expect(groupBody.scope().isOpen).toBeFalsy();
findGroupLink(0).click();
scope.$digest();
expect(groupBody.scope().isOpen).toBeTruthy();
});
it('should have text-muted styling', function() {
expect(findGroupLink(0).find('span:first')).toHaveClass('text-muted');
});
});
// This is re-used in both the uib-accordion-heading element and the uib-accordion-heading attribute tests
function isDisabledStyleCheck() {
var tpl =
'' +
'' +
'Heading Element {{x}} ' +
'Body' +
'
' +
'';
scope.disabled = true;
element = $compile(tpl)(scope);
scope.$digest();
groups = element.find('.panel');
expect(findGroupLink(0).find('span').hasClass('text-muted')).toBe(true);
}
describe('uib-accordion-heading element', function() {
beforeEach(function() {
var tpl =
'' +
'' +
'Heading Element {{x}} ' +
'Body' +
'
' +
'';
element = $compile(tpl)(scope);
scope.$digest();
groups = element.find('.panel');
});
it('transcludes the content into the heading link', function() {
expect(findGroupLink(0).text()).toBe('Heading Element 123 ');
});
it('attaches the same scope to the transcluded heading and body', function() {
expect(findGroupLink(0).scope().$id).toBe(findGroupBody(0).scope().$id);
});
it('should wrap the transcluded content in a span', function() {
expect(findGroupLink(0).find('span:first').length).toEqual(1);
});
it('should have disabled styling when is-disabled is true', isDisabledStyleCheck);
});
describe('uib-accordion-heading attribute', function() {
beforeEach(function() {
var tpl =
'' +
'' +
'
Heading Element {{x}}
' +
'Body' +
'
' +
'';
element = $compile(tpl)(scope);
scope.$digest();
groups = element.find('.panel');
});
it('transcludes the content into the heading link', function() {
expect(findGroupLink(0).text()).toBe('Heading Element 123 ');
});
it('attaches the same scope to the transcluded heading and body', function() {
expect(findGroupLink(0).scope().$id).toBe(findGroupBody(0).scope().$id);
});
it('should have disabled styling when is-disabled is true', isDisabledStyleCheck);
});
describe('uib-accordion-heading, with repeating uib-accordion-groups', function() {
it('should clone the uib-accordion-heading for each group', function() {
element = $compile('{{x}}
')(scope);
scope.$digest();
groups = element.find('.panel');
expect(groups.length).toBe(3);
expect(findGroupLink(0).text()).toBe('1');
expect(findGroupLink(1).text()).toBe('2');
expect(findGroupLink(2).text()).toBe('3');
});
});
describe('uib-accordion-heading attribute, with repeating uib-accordion-groups', function() {
it('should clone the uib-accordion-heading for each group', function() {
element = $compile('')(scope);
scope.$digest();
groups = element.find('.panel');
expect(groups.length).toBe(3);
expect(findGroupLink(0).text()).toBe('1');
expect(findGroupLink(1).text()).toBe('2');
expect(findGroupLink(2).text()).toBe('3');
});
});
describe('uib-accordion-heading attribute, with custom template', function() {
it('should transclude heading to a template using data-uib-accordion-header', inject(function($templateCache) {
$templateCache.put('foo/bar.html', '');
element = $compile('baz
')(scope);
scope.$digest();
groups = element.find('.panel');
expect(findGroupLink(0).text()).toBe('baz');
}));
});
});
});
================================================
FILE: src/alert/alert.js
================================================
angular.module('ui.bootstrap.alert', [])
.controller('UibAlertController', ['$scope', '$element', '$attrs', '$interpolate', '$timeout', function($scope, $element, $attrs, $interpolate, $timeout) {
$scope.closeable = !!$attrs.close;
$element.addClass('alert');
$attrs.$set('role', 'alert');
if ($scope.closeable) {
$element.addClass('alert-dismissible');
}
var dismissOnTimeout = angular.isDefined($attrs.dismissOnTimeout) ?
$interpolate($attrs.dismissOnTimeout)($scope.$parent) : null;
if (dismissOnTimeout) {
$timeout(function() {
$scope.close();
}, parseInt(dismissOnTimeout, 10));
}
}])
.directive('uibAlert', function() {
return {
controller: 'UibAlertController',
controllerAs: 'alert',
restrict: 'A',
templateUrl: function(element, attrs) {
return attrs.templateUrl || 'uib/template/alert/alert.html';
},
transclude: true,
scope: {
close: '&'
}
};
});
================================================
FILE: src/alert/docs/demo.html
================================================
{{alert.msg}}
A happy alert!
================================================
FILE: src/alert/docs/demo.js
================================================
angular.module('ui.bootstrap.demo').controller('AlertDemoCtrl', function ($scope) {
$scope.alerts = [
{ type: 'danger', msg: 'Oh snap! Change a few things up and try submitting again.' },
{ type: 'success', msg: 'Well done! You successfully read this important alert message.' }
];
$scope.addAlert = function() {
$scope.alerts.push({msg: 'Another alert!'});
};
$scope.closeAlert = function(index) {
$scope.alerts.splice(index, 1);
};
});
================================================
FILE: src/alert/docs/readme.md
================================================
This directive can be used both to generate alerts from static and dynamic model data (using the `ng-repeat` directive).
### uib-alert settings
* `close()`
$ -
A callback function that gets fired when an `alert` is closed. If the attribute exists, a close button is displayed as well.
* `dismiss-on-timeout`
_(Default: `none`)_ -
Takes the number of milliseconds that specify the timeout duration, after which the alert will be closed. This attribute requires the presence of the `close` attribute.
* `template-url`
_(Default: `uib/template/alert/alert.html`)_ -
Add the ability to override the template used in the component.
================================================
FILE: src/alert/index.js
================================================
require('../../template/alert/alert.html.js');
require('./alert');
var MODULE_NAME = 'ui.bootstrap.module.alert';
angular.module(MODULE_NAME, ['ui.bootstrap.alert', 'uib/template/alert/alert.html']);
module.exports = MODULE_NAME;
================================================
FILE: src/alert/test/alert.spec.js
================================================
describe('uib-alert', function() {
var element, scope, $compile, $templateCache, $timeout;
beforeEach(module('ui.bootstrap.alert'));
beforeEach(module('uib/template/alert/alert.html'));
beforeEach(inject(function($rootScope, _$compile_, _$templateCache_, _$timeout_) {
scope = $rootScope;
$compile = _$compile_;
$templateCache = _$templateCache_;
$timeout = _$timeout_;
element = angular.element(
'' +
'
{{alert.msg}}' +
'
' +
'
');
scope.alerts = [
{ msg:'foo', type:'success'},
{ msg:'bar', type:'error'},
{ msg:'baz'}
];
}));
function createAlerts() {
$compile(element)(scope);
scope.$digest();
return element.find('.alert');
}
function findCloseButton(index) {
return element.find('.close').eq(index);
}
function findContent(index) {
return element.find('div[ng-transclude]').eq(index);
}
it('should expose the controller to the view', function() {
$templateCache.put('uib/template/alert/alert.html', '{{alert.text}}
');
element = $compile('')(scope);
scope.$digest();
var ctrl = element.controller('uib-alert');
expect(ctrl).toBeDefined();
ctrl.text = 'foo';
scope.$digest();
expect(element.html()).toBe('foo
');
});
it('should support custom templates', function() {
$templateCache.put('foo/bar.html', 'baz
');
element = $compile('')(scope);
scope.$digest();
expect(element.html()).toBe('baz
');
});
it('should generate alerts using ng-repeat', function() {
var alerts = createAlerts();
expect(alerts.length).toEqual(3);
});
it('should show the alert content', function() {
var alerts = createAlerts();
for (var i = 0, n = alerts.length; i < n; i++) {
expect(findContent(i).text()).toBe(scope.alerts[i].msg);
}
});
it('should show close buttons and have the dismissible class', function() {
var alerts = createAlerts();
for (var i = 0, n = alerts.length; i < n; i++) {
expect(findCloseButton(i).css('display')).not.toBe('none');
expect(alerts.eq(i)).toHaveClass('alert-dismissible');
}
});
it('should fire callback when closed', function() {
var alerts = createAlerts();
scope.$apply(function() {
scope.removeAlert = jasmine.createSpy();
});
expect(findCloseButton(0).css('display')).not.toBe('none');
findCloseButton(1).click();
expect(scope.removeAlert).toHaveBeenCalledWith(1);
});
it('should not show close button and have the dismissible class if no close callback specified', function() {
element = $compile('No close
')(scope);
scope.$digest();
expect(findCloseButton(0)).toBeHidden();
expect(element).not.toHaveClass('alert-dismissible');
});
it('should close automatically if dismiss-on-timeout is defined on the element', function() {
scope.removeAlert = jasmine.createSpy();
$compile('Default alert!
')(scope);
scope.$digest();
$timeout.flush();
expect(scope.removeAlert).toHaveBeenCalled();
});
it('should not close immediately with a dynamic dismiss-on-timeout', function() {
scope.removeAlert = jasmine.createSpy();
scope.dismissTime = 500;
$compile('Default alert!
')(scope);
scope.$digest();
$timeout.flush(100);
expect(scope.removeAlert).not.toHaveBeenCalled();
$timeout.flush(500);
expect(scope.removeAlert).toHaveBeenCalled();
});
});
================================================
FILE: src/buttons/buttons.js
================================================
angular.module('ui.bootstrap.buttons', [])
.constant('uibButtonConfig', {
activeClass: 'active',
toggleEvent: 'click'
})
.controller('UibButtonsController', ['uibButtonConfig', function(buttonConfig) {
this.activeClass = buttonConfig.activeClass || 'active';
this.toggleEvent = buttonConfig.toggleEvent || 'click';
}])
.directive('uibBtnRadio', ['$parse', function($parse) {
return {
require: ['uibBtnRadio', 'ngModel'],
controller: 'UibButtonsController',
controllerAs: 'buttons',
link: function(scope, element, attrs, ctrls) {
var buttonsCtrl = ctrls[0], ngModelCtrl = ctrls[1];
var uncheckableExpr = $parse(attrs.uibUncheckable);
element.find('input').css({display: 'none'});
//model -> UI
ngModelCtrl.$render = function() {
element.toggleClass(buttonsCtrl.activeClass, angular.equals(ngModelCtrl.$modelValue, scope.$eval(attrs.uibBtnRadio)));
};
//ui->model
element.on(buttonsCtrl.toggleEvent, function() {
if (attrs.disabled) {
return;
}
var isActive = element.hasClass(buttonsCtrl.activeClass);
if (!isActive || angular.isDefined(attrs.uncheckable)) {
scope.$apply(function() {
ngModelCtrl.$setViewValue(isActive ? null : scope.$eval(attrs.uibBtnRadio));
ngModelCtrl.$render();
});
}
});
if (attrs.uibUncheckable) {
scope.$watch(uncheckableExpr, function(uncheckable) {
attrs.$set('uncheckable', uncheckable ? '' : undefined);
});
}
}
};
}])
.directive('uibBtnCheckbox', function() {
return {
require: ['uibBtnCheckbox', 'ngModel'],
controller: 'UibButtonsController',
controllerAs: 'button',
link: function(scope, element, attrs, ctrls) {
var buttonsCtrl = ctrls[0], ngModelCtrl = ctrls[1];
element.find('input').css({display: 'none'});
function getTrueValue() {
return getCheckboxValue(attrs.btnCheckboxTrue, true);
}
function getFalseValue() {
return getCheckboxValue(attrs.btnCheckboxFalse, false);
}
function getCheckboxValue(attribute, defaultValue) {
return angular.isDefined(attribute) ? scope.$eval(attribute) : defaultValue;
}
//model -> UI
ngModelCtrl.$render = function() {
element.toggleClass(buttonsCtrl.activeClass, angular.equals(ngModelCtrl.$modelValue, getTrueValue()));
};
//ui->model
element.on(buttonsCtrl.toggleEvent, function() {
if (attrs.disabled) {
return;
}
scope.$apply(function() {
ngModelCtrl.$setViewValue(element.hasClass(buttonsCtrl.activeClass) ? getFalseValue() : getTrueValue());
ngModelCtrl.$render();
});
});
}
};
});
================================================
FILE: src/buttons/docs/demo.html
================================================
Single toggle
{{singleModel}}
Checkbox
Model: {{checkModel}}
Results: {{checkResults}}
Radio & Uncheckable Radio
{{radioModel || 'null'}}
================================================
FILE: src/buttons/docs/demo.js
================================================
angular.module('ui.bootstrap.demo').controller('ButtonsCtrl', function ($scope) {
$scope.singleModel = 1;
$scope.radioModel = 'Middle';
$scope.checkModel = {
left: false,
middle: true,
right: false
};
$scope.checkResults = [];
$scope.$watchCollection('checkModel', function () {
$scope.checkResults = [];
angular.forEach($scope.checkModel, function (value, key) {
if (value) {
$scope.checkResults.push(key);
}
});
});
});
================================================
FILE: src/buttons/docs/readme.md
================================================
With the buttons directive, we can make a group of buttons behave like a set of checkboxes (`uib-btn-checkbox`) or behave like a set of radio buttons (`uib-btn-radio`).
### uib-btn-checkbox settings
* `btn-checkbox-false`
_(Default: `false`)_ -
Sets the value for the unchecked status.
* `btn-checkbox-true`
_(Default: `true`)_ -
Sets the value for the checked status.
* `ng-model`
$
-
Model where we set the checkbox status. By default `true` or `false`.
### uib-btn-radio settings
* `ng-model`
$
-
Model where we set the radio status. All radio buttons in a group should use the same `ng-model`.
* `uib-btn-radio` -
$
Value to assign to the `ng-model` if we check this radio button.
* `uib-uncheckable`
$
_(Default: `null`)_ -
An expression that evaluates to a truthy or falsy value that determines whether the `uncheckable` attribute is present.
* `uncheckable`
B -
Whether a radio button can be unchecked or not.
### Additional settings `uibButtonConfig`
* `activeClass`
_(Default: `active`)_ -
Class to apply to the checked buttons.
* `toggleEvent`
_(Default: `click`)_ -
Event used to toggle the buttons.
### Known issues
To use tooltips or popovers on elements within a `btn-group`, set the tooltip/popover `appendToBody` option to `true`. This is due to Bootstrap CSS styling. See [here](http://getbootstrap.com/components/#btn-groups) for more information.
================================================
FILE: src/buttons/index.js
================================================
require('./buttons');
var MODULE_NAME = 'ui.bootstrap.module.buttons';
angular.module(MODULE_NAME, ['ui.bootstrap.buttons']);
module.exports = MODULE_NAME;
================================================
FILE: src/buttons/test/buttons.spec.js
================================================
describe('buttons', function() {
var $scope, $compile;
beforeEach(module('ui.bootstrap.buttons'));
beforeEach(inject(function(_$rootScope_, _$compile_) {
$scope = _$rootScope_;
$compile = _$compile_;
}));
describe('checkbox', function() {
var compileButton = function(markup, scope) {
var el = $compile(markup)(scope);
scope.$digest();
return el;
};
it('should expose the controller to the view', inject(function($templateCache) {
var btn = compileButton('', $scope);
var ctrl = btn.controller('uibBtnCheckbox');
expect(ctrl).toBeDefined();
ctrl.text = 'foo';
$scope.$digest();
expect(btn.html()).toBe('foo');
}));
//model -> UI
it('should work correctly with default model values', function() {
$scope.model = false;
var btn = compileButton('', $scope);
expect(btn).not.toHaveClass('active');
$scope.model = true;
$scope.$digest();
expect(btn).toHaveClass('active');
});
it('should bind custom model values', function() {
$scope.model = 1;
var btn = compileButton('', $scope);
expect(btn).toHaveClass('active');
$scope.model = 0;
$scope.$digest();
expect(btn).not.toHaveClass('active');
});
//UI-> model
it('should toggle default model values on click', function() {
$scope.model = false;
var btn = compileButton('', $scope);
btn.click();
expect($scope.model).toEqual(true);
expect(btn).toHaveClass('active');
btn.click();
expect($scope.model).toEqual(false);
expect(btn).not.toHaveClass('active');
});
it('should toggle custom model values on click', function() {
$scope.model = 0;
var btn = compileButton('', $scope);
btn.click();
expect($scope.model).toEqual(1);
expect(btn).toHaveClass('active');
btn.click();
expect($scope.model).toEqual(0);
expect(btn).not.toHaveClass('active');
});
it('should monitor true / false value changes - issue 666', function() {
$scope.model = 1;
$scope.trueVal = 1;
var btn = compileButton('', $scope);
expect(btn).toHaveClass('active');
expect($scope.model).toEqual(1);
$scope.model = 2;
$scope.trueVal = 2;
$scope.$digest();
expect(btn).toHaveClass('active');
expect($scope.model).toEqual(2);
});
it('should not toggle when disabled - issue 4013', function() {
$scope.model = 1;
$scope.falseVal = 0;
var btn = compileButton('', $scope);
expect(btn).not.toHaveClass('active');
expect($scope.model).toEqual(1);
btn.click();
expect(btn).not.toHaveClass('active');
$scope.$digest();
expect(btn).not.toHaveClass('active');
});
describe('setting buttonConfig', function() {
var uibButtonConfig, originalActiveClass, originalToggleEvent;
beforeEach(inject(function(_uibButtonConfig_) {
uibButtonConfig = _uibButtonConfig_;
originalActiveClass = uibButtonConfig.activeClass;
originalToggleEvent = uibButtonConfig.toggleEvent;
uibButtonConfig.activeClass = false;
uibButtonConfig.toggleEvent = false;
}));
afterEach(function() {
// return it to the original value
uibButtonConfig.activeClass = originalActiveClass;
uibButtonConfig.toggleEvent = originalToggleEvent;
});
it('should use default config when buttonConfig.activeClass and buttonConfig.toggleEvent is false', function() {
$scope.model = false;
var btn = compileButton('', $scope);
expect(btn).not.toHaveClass('active');
$scope.model = true;
$scope.$digest();
expect(btn).toHaveClass('active');
});
it('should be able to use a different active class', function() {
uibButtonConfig.activeClass = 'foo';
$scope.model = false;
var btn = compileButton('', $scope);
expect(btn).not.toHaveClass('foo');
$scope.model = true;
$scope.$digest();
expect(btn).toHaveClass('foo');
});
it('should be able to use a different toggle event', function() {
uibButtonConfig.toggleEvent = 'mouseenter';
$scope.model = false;
var btn = compileButton('', $scope);
expect(btn).not.toHaveClass('active');
btn.trigger('mouseenter');
$scope.$digest();
expect(btn).toHaveClass('active');
});
});
});
describe('radio', function() {
var compileButtons = function(markup, scope) {
var el = $compile(''+markup+'
')(scope);
scope.$digest();
return el.find('button');
};
it('should expose the controller to the view', inject(function($templateCache) {
var btn = compileButtons('', $scope);
var ctrl = btn.controller('uibBtnRadio');
expect(ctrl).toBeDefined();
ctrl.text = 'foo';
$scope.$digest();
expect(btn.html()).toBe('foo');
}));
//model -> UI
it('should set active class based on model', function() {
var btns = compileButtons('', $scope);
expect(btns.eq(0)).not.toHaveClass('active');
expect(btns.eq(1)).not.toHaveClass('active');
$scope.model = 2;
$scope.$digest();
expect(btns.eq(0)).not.toHaveClass('active');
expect(btns.eq(1)).toHaveClass('active');
});
//UI->model
it('should set active class via click', function() {
var btns = compileButtons('', $scope);
expect($scope.model).toBeUndefined();
btns.eq(0).click();
expect($scope.model).toEqual(1);
expect(btns.eq(0)).toHaveClass('active');
expect(btns.eq(1)).not.toHaveClass('active');
btns.eq(1).click();
expect($scope.model).toEqual(2);
expect(btns.eq(1)).toHaveClass('active');
expect(btns.eq(0)).not.toHaveClass('active');
});
it('should watch uib-btn-radio values and update state accordingly', function() {
$scope.values = ['value1', 'value2'];
var btns = compileButtons('', $scope);
expect(btns.eq(0)).not.toHaveClass('active');
expect(btns.eq(1)).not.toHaveClass('active');
$scope.model = 'value2';
$scope.$digest();
expect(btns.eq(0)).not.toHaveClass('active');
expect(btns.eq(1)).toHaveClass('active');
$scope.values[1] = 'value3';
$scope.model = 'value3';
$scope.$digest();
expect(btns.eq(0)).not.toHaveClass('active');
expect(btns.eq(1)).toHaveClass('active');
});
it('should do nothing when clicking an active radio', function() {
$scope.model = 1;
var btns = compileButtons('', $scope);
expect(btns.eq(0)).toHaveClass('active');
expect(btns.eq(1)).not.toHaveClass('active');
btns.eq(0).click();
$scope.$digest();
expect(btns.eq(0)).toHaveClass('active');
expect(btns.eq(1)).not.toHaveClass('active');
});
it('should not toggle when disabled - issue 4013', function() {
$scope.model = 1;
var btns = compileButtons('', $scope);
expect(btns.eq(0)).toHaveClass('active');
expect(btns.eq(1)).not.toHaveClass('active');
btns.eq(1).click();
expect(btns.eq(0)).toHaveClass('active');
expect(btns.eq(1)).not.toHaveClass('active');
$scope.$digest();
expect(btns.eq(0)).toHaveClass('active');
expect(btns.eq(1)).not.toHaveClass('active');
});
it('should handle string values in uib-btn-radio value', function() {
$scope.model = 'Two';
var btns = compileButtons('', $scope);
expect(btns.eq(0)).not.toHaveClass('active');
expect(btns.eq(1)).toHaveClass('active');
btns.eq(0).click();
expect(btns.eq(0)).toHaveClass('active');
expect(btns.eq(1)).not.toHaveClass('active');
expect($scope.model).toEqual('One');
$scope.$digest();
expect(btns.eq(0)).toHaveClass('active');
expect(btns.eq(1)).not.toHaveClass('active');
expect($scope.model).toEqual('One');
});
describe('uncheckable', function() {
//model -> UI
it('should set active class based on model', function() {
var btns = compileButtons('', $scope);
expect(btns.eq(0)).not.toHaveClass('active');
expect(btns.eq(1)).not.toHaveClass('active');
$scope.model = 2;
$scope.$digest();
expect(btns.eq(0)).not.toHaveClass('active');
expect(btns.eq(1)).toHaveClass('active');
});
//UI->model
it('should unset active class via click', function() {
var btns = compileButtons('', $scope);
expect($scope.model).toBeUndefined();
btns.eq(0).click();
expect($scope.model).toEqual(1);
expect(btns.eq(0)).toHaveClass('active');
expect(btns.eq(1)).not.toHaveClass('active');
btns.eq(0).click();
expect($scope.model).toBeNull();
expect(btns.eq(1)).not.toHaveClass('active');
expect(btns.eq(0)).not.toHaveClass('active');
});
it('should watch uib-btn-radio values and update state', function() {
$scope.values = ['value1', 'value2'];
var btns = compileButtons('', $scope);
expect(btns.eq(0)).not.toHaveClass('active');
expect(btns.eq(1)).not.toHaveClass('active');
$scope.model = 'value2';
$scope.$digest();
expect(btns.eq(0)).not.toHaveClass('active');
expect(btns.eq(1)).toHaveClass('active');
$scope.model = undefined;
$scope.$digest();
expect(btns.eq(0)).not.toHaveClass('active');
expect(btns.eq(1)).not.toHaveClass('active');
});
});
describe('uibUncheckable', function() {
it('should set uncheckable', function() {
$scope.uncheckable = false;
var btns = compileButtons('', $scope);
expect(btns.eq(0).attr('uncheckable')).toBeUndefined();
expect(btns.eq(1).attr('uncheckable')).toBeUndefined();
expect($scope.model).toBeUndefined();
btns.eq(0).click();
expect($scope.model).toEqual(1);
btns.eq(0).click();
expect($scope.model).toEqual(1);
btns.eq(1).click();
expect($scope.model).toEqual(2);
btns.eq(1).click();
expect($scope.model).toEqual(2);
$scope.uncheckable = true;
$scope.$digest();
expect(btns.eq(0).attr('uncheckable')).toBeUndefined();
expect(btns.eq(1).attr('uncheckable')).toBeDefined();
btns.eq(0).click();
expect($scope.model).toEqual(1);
btns.eq(0).click();
expect($scope.model).toEqual(1);
btns.eq(1).click();
expect($scope.model).toEqual(2);
btns.eq(1).click();
expect($scope.model).toBeNull();
});
});
});
});
================================================
FILE: src/carousel/carousel.css
================================================
.ng-animate.item:not(.left):not(.right) {
-webkit-transition: 0s ease-in-out left;
transition: 0s ease-in-out left
}
================================================
FILE: src/carousel/carousel.js
================================================
angular.module('ui.bootstrap.carousel', [])
.controller('UibCarouselController', ['$scope', '$element', '$interval', '$timeout', '$animate', function($scope, $element, $interval, $timeout, $animate) {
var self = this,
slides = self.slides = $scope.slides = [],
SLIDE_DIRECTION = 'uib-slideDirection',
currentIndex = $scope.active,
currentInterval, isPlaying;
var destroyed = false;
$element.addClass('carousel');
self.addSlide = function(slide, element) {
slides.push({
slide: slide,
element: element
});
slides.sort(function(a, b) {
return +a.slide.index - +b.slide.index;
});
//if this is the first slide or the slide is set to active, select it
if (slide.index === $scope.active || slides.length === 1 && !angular.isNumber($scope.active)) {
if ($scope.$currentTransition) {
$scope.$currentTransition = null;
}
currentIndex = slide.index;
$scope.active = slide.index;
setActive(currentIndex);
self.select(slides[findSlideIndex(slide)]);
if (slides.length === 1) {
$scope.play();
}
}
};
self.getCurrentIndex = function() {
for (var i = 0; i < slides.length; i++) {
if (slides[i].slide.index === currentIndex) {
return i;
}
}
};
self.next = $scope.next = function() {
var newIndex = (self.getCurrentIndex() + 1) % slides.length;
if (newIndex === 0 && $scope.noWrap()) {
$scope.pause();
return;
}
return self.select(slides[newIndex], 'next');
};
self.prev = $scope.prev = function() {
var newIndex = self.getCurrentIndex() - 1 < 0 ? slides.length - 1 : self.getCurrentIndex() - 1;
if ($scope.noWrap() && newIndex === slides.length - 1) {
$scope.pause();
return;
}
return self.select(slides[newIndex], 'prev');
};
self.removeSlide = function(slide) {
var index = findSlideIndex(slide);
//get the index of the slide inside the carousel
slides.splice(index, 1);
if (slides.length > 0 && currentIndex === index) {
if (index >= slides.length) {
currentIndex = slides.length - 1;
$scope.active = currentIndex;
setActive(currentIndex);
self.select(slides[slides.length - 1]);
} else {
currentIndex = index;
$scope.active = currentIndex;
setActive(currentIndex);
self.select(slides[index]);
}
} else if (currentIndex > index) {
currentIndex--;
$scope.active = currentIndex;
}
//clean the active value when no more slide
if (slides.length === 0) {
currentIndex = null;
$scope.active = null;
}
};
/* direction: "prev" or "next" */
self.select = $scope.select = function(nextSlide, direction) {
var nextIndex = findSlideIndex(nextSlide.slide);
//Decide direction if it's not given
if (direction === undefined) {
direction = nextIndex > self.getCurrentIndex() ? 'next' : 'prev';
}
//Prevent this user-triggered transition from occurring if there is already one in progress
if (nextSlide.slide.index !== currentIndex &&
!$scope.$currentTransition) {
goNext(nextSlide.slide, nextIndex, direction);
}
};
/* Allow outside people to call indexOf on slides array */
$scope.indexOfSlide = function(slide) {
return +slide.slide.index;
};
$scope.isActive = function(slide) {
return $scope.active === slide.slide.index;
};
$scope.isPrevDisabled = function() {
return $scope.active === 0 && $scope.noWrap();
};
$scope.isNextDisabled = function() {
return $scope.active === slides.length - 1 && $scope.noWrap();
};
$scope.pause = function() {
if (!$scope.noPause) {
isPlaying = false;
resetTimer();
}
};
$scope.play = function() {
if (!isPlaying) {
isPlaying = true;
restartTimer();
}
};
$element.on('mouseenter', $scope.pause);
$element.on('mouseleave', $scope.play);
$scope.$on('$destroy', function() {
destroyed = true;
resetTimer();
});
$scope.$watch('noTransition', function(noTransition) {
$animate.enabled($element, !noTransition);
});
$scope.$watch('interval', restartTimer);
$scope.$watchCollection('slides', resetTransition);
$scope.$watch('active', function(index) {
if (angular.isNumber(index) && currentIndex !== index) {
for (var i = 0; i < slides.length; i++) {
if (slides[i].slide.index === index) {
index = i;
break;
}
}
var slide = slides[index];
if (slide) {
setActive(index);
self.select(slides[index]);
currentIndex = index;
}
}
});
function getSlideByIndex(index) {
for (var i = 0, l = slides.length; i < l; ++i) {
if (slides[i].index === index) {
return slides[i];
}
}
}
function setActive(index) {
for (var i = 0; i < slides.length; i++) {
slides[i].slide.active = i === index;
}
}
function goNext(slide, index, direction) {
if (destroyed) {
return;
}
angular.extend(slide, {direction: direction});
angular.extend(slides[currentIndex].slide || {}, {direction: direction});
if ($animate.enabled($element) && !$scope.$currentTransition &&
slides[index].element && self.slides.length > 1) {
slides[index].element.data(SLIDE_DIRECTION, slide.direction);
var currentIdx = self.getCurrentIndex();
if (angular.isNumber(currentIdx) && slides[currentIdx].element) {
slides[currentIdx].element.data(SLIDE_DIRECTION, slide.direction);
}
$scope.$currentTransition = true;
$animate.on('addClass', slides[index].element, function(element, phase) {
if (phase === 'close') {
$scope.$currentTransition = null;
$animate.off('addClass', element);
}
});
}
$scope.active = slide.index;
currentIndex = slide.index;
setActive(index);
//every time you change slides, reset the timer
restartTimer();
}
function findSlideIndex(slide) {
for (var i = 0; i < slides.length; i++) {
if (slides[i].slide === slide) {
return i;
}
}
}
function resetTimer() {
if (currentInterval) {
$interval.cancel(currentInterval);
currentInterval = null;
}
}
function resetTransition(slides) {
if (!slides.length) {
$scope.$currentTransition = null;
}
}
function restartTimer() {
resetTimer();
var interval = +$scope.interval;
if (!isNaN(interval) && interval > 0) {
currentInterval = $interval(timerFn, interval);
}
}
function timerFn() {
var interval = +$scope.interval;
if (isPlaying && !isNaN(interval) && interval > 0 && slides.length) {
$scope.next();
} else {
$scope.pause();
}
}
}])
.directive('uibCarousel', function() {
return {
transclude: true,
controller: 'UibCarouselController',
controllerAs: 'carousel',
restrict: 'A',
templateUrl: function(element, attrs) {
return attrs.templateUrl || 'uib/template/carousel/carousel.html';
},
scope: {
active: '=',
interval: '=',
noTransition: '=',
noPause: '=',
noWrap: '&'
}
};
})
.directive('uibSlide', ['$animate', function($animate) {
return {
require: '^uibCarousel',
restrict: 'A',
transclude: true,
templateUrl: function(element, attrs) {
return attrs.templateUrl || 'uib/template/carousel/slide.html';
},
scope: {
actual: '=?',
index: '=?'
},
link: function (scope, element, attrs, carouselCtrl) {
element.addClass('item');
carouselCtrl.addSlide(scope, element);
//when the scope is destroyed then remove the slide from the current slides array
scope.$on('$destroy', function() {
carouselCtrl.removeSlide(scope);
});
scope.$watch('active', function(active) {
$animate[active ? 'addClass' : 'removeClass'](element, 'active');
});
}
};
}])
.animation('.item', ['$animateCss',
function($animateCss) {
var SLIDE_DIRECTION = 'uib-slideDirection';
function removeClass(element, className, callback) {
element.removeClass(className);
if (callback) {
callback();
}
}
return {
beforeAddClass: function(element, className, done) {
if (className === 'active') {
var stopped = false;
var direction = element.data(SLIDE_DIRECTION);
var directionClass = direction === 'next' ? 'left' : 'right';
var removeClassFn = removeClass.bind(this, element,
directionClass + ' ' + direction, done);
element.addClass(direction);
$animateCss(element, {addClass: directionClass})
.start()
.done(removeClassFn);
return function() {
stopped = true;
};
}
done();
},
beforeRemoveClass: function (element, className, done) {
if (className === 'active') {
var stopped = false;
var direction = element.data(SLIDE_DIRECTION);
var directionClass = direction === 'next' ? 'left' : 'right';
var removeClassFn = removeClass.bind(this, element, directionClass, done);
$animateCss(element, {addClass: directionClass})
.start()
.done(removeClassFn);
return function() {
stopped = true;
};
}
done();
}
};
}]);
================================================
FILE: src/carousel/docs/README.md
================================================
Carousel creates a carousel similar to bootstrap's image carousel.
The carousel also offers support for touchscreen devices in the form of swiping. To enable swiping, load the `ngTouch` module as a dependency.
Use a `` element with `` elements inside it.
### uib-carousel settings
* `active`
_(Default: `Index of first slide`)_ -
Index of current active slide.
* `interval`
$
_(Default: `none`)_ -
Sets an interval to cycle through the slides. You need a number bigger than 0 to make the interval work.
* `no-pause`
$
_(Default: `false`)_ -
The interval pauses on mouseover. Setting this to truthy, disables this pause.
* `no-transition`
$
_(Default: `false`)_ -
Whether to disable the transition animation between slides. Setting this to truthy, disables this transition.
* `no-wrap`
$
_(Default: `false`)_ -
Disables the looping of slides. Setting `no-wrap` to an expression which evaluates to a truthy value will prevent looping.
* `template-url`
_(Default: `uib/template/carousel/carousel.html`)_ -
Add the ability to override the template used on the component.
### uib-slide settings
* `actual`
$
_(Default: `none`)_ -
Use this attribute to bind the slide model (or any object of interest) onto the slide scope, which makes it available for customization in the carousel template.
* `index`
$
_(Default: `none`)_ -
The index of the slide. Must be unique.
* `template-url`
_(Default: `uib/template/carousel/slide.html`)_ -
Add the ability to override the template used on the component.
================================================
FILE: src/carousel/docs/demo.html
================================================
Slide {{slide.id}}
{{slide.text}}
================================================
FILE: src/carousel/docs/demo.js
================================================
angular.module('ui.bootstrap.demo').controller('CarouselDemoCtrl', function ($scope) {
$scope.myInterval = 5000;
$scope.noWrapSlides = false;
$scope.active = 0;
var slides = $scope.slides = [];
var currIndex = 0;
$scope.addSlide = function() {
var newWidth = 600 + slides.length + 1;
slides.push({
image: '//unsplash.it/' + newWidth + '/300',
text: ['Nice image','Awesome photograph','That is so cool','I love that'][slides.length % 4],
id: currIndex++
});
};
$scope.randomize = function() {
var indexes = generateIndexesArray();
assignNewIndexesToSlides(indexes);
};
for (var i = 0; i < 4; i++) {
$scope.addSlide();
}
// Randomize logic below
function assignNewIndexesToSlides(indexes) {
for (var i = 0, l = slides.length; i < l; i++) {
slides[i].id = indexes.pop();
}
}
function generateIndexesArray() {
var indexes = [];
for (var i = 0; i < currIndex; ++i) {
indexes[i] = i;
}
return shuffle(indexes);
}
// http://stackoverflow.com/questions/962802#962890
function shuffle(array) {
var tmp, current, top = array.length;
if (top) {
while (--top) {
current = Math.floor(Math.random() * (top + 1));
tmp = array[current];
array[current] = array[top];
array[top] = tmp;
}
}
return array;
}
});
================================================
FILE: src/carousel/index-nocss.js
================================================
require('../../template/carousel/carousel.html.js');
require('../../template/carousel/slide.html.js');
require('./carousel');
var MODULE_NAME = 'ui.bootstrap.module.carousel';
angular.module(MODULE_NAME, ['ui.bootstrap.carousel', 'uib/template/carousel/carousel.html', 'uib/template/carousel/slide.html']);
module.exports = MODULE_NAME;
================================================
FILE: src/carousel/index.js
================================================
require('./carousel.css');
module.exports = require('./index-nocss.js');
================================================
FILE: src/carousel/test/carousel.spec.js
================================================
describe('carousel', function() {
beforeEach(module('ui.bootstrap.carousel'));
beforeEach(module('ngAnimateMock'));
beforeEach(module('uib/template/carousel/carousel.html', 'uib/template/carousel/slide.html'));
var $rootScope, $compile, $controller, $interval, $templateCache, $timeout, $animate;
beforeEach(inject(function(_$rootScope_, _$compile_, _$controller_, _$interval_, _$templateCache_, _$timeout_, _$animate_) {
$rootScope = _$rootScope_;
$compile = _$compile_;
$controller = _$controller_;
$interval = _$interval_;
$templateCache = _$templateCache_;
$timeout = _$timeout_;
$animate = _$animate_;
}));
describe('basics', function() {
var elm, scope;
beforeEach(function() {
scope = $rootScope.$new();
scope.slides = [
{content: 'one', index: 0},
{content: 'two', index: 1},
{content: 'three', index: 2}
];
elm = $compile(
'' +
'
' +
'{{slide.content}}' +
'
' +
'
'
)(scope);
scope.interval = 5000;
scope.nopause = undefined;
scope.$apply();
});
function testSlideActive(slideIndex) {
for (var i = 0; i < scope.slides.length; i++) {
if (i === slideIndex) {
expect(scope.active).toBe(scope.slides[i].index);
} else {
expect(scope.active).not.toBe(scope.slides[i].index);
}
}
}
it('should allow overriding of the carousel template', function() {
$templateCache.put('foo/bar.html', 'foo
');
elm = $compile('')(scope);
$rootScope.$digest();
expect(elm.html()).toBe('foo
');
});
it('should allow overriding of the slide template', function() {
$templateCache.put('foo/bar.html', 'bar
');
elm = $compile(
''
)(scope);
$rootScope.$digest();
var slide = elm.find('.slide');
expect(slide.html()).toBe('bar');
});
it('should be able to select a slide via model changes', function() {
testSlideActive(0);
scope.$apply('active=1');
testSlideActive(1);
});
it('should create clickable prev nav button', function() {
var navPrev = elm.find('a.left');
var navNext = elm.find('a.right');
expect(navPrev.length).toBe(1);
expect(navNext.length).toBe(1);
});
it('should display clickable slide indicators', function () {
var indicators = elm.find('ol.carousel-indicators > li');
expect(indicators.length).toBe(3);
});
it('should stop cycling slides forward when noWrap is truthy', function () {
elm = $compile(
'' +
'
' +
'{{slide.content}}' +
'
' +
'
'
)(scope);
scope.noWrap = true;
scope.$apply();
var $scope = elm.isolateScope();
spyOn($scope, 'pause');
scope.active = $scope.slides.length - 1;
scope.$apply();
testSlideActive($scope.slides.length - 1);
$scope.next();
testSlideActive($scope.slides.length - 1);
expect($scope.pause).toHaveBeenCalled();
});
it('should stop cycling slides backward when noWrap is truthy', function () {
elm = $compile(
'' +
'
' +
'{{slide.content}}' +
'
' +
'
'
)(scope);
scope.noWrap = true;
scope.$apply();
var $scope = elm.isolateScope();
spyOn($scope, 'pause');
testSlideActive(0);
$scope.prev();
testSlideActive(0);
expect($scope.pause).toHaveBeenCalled();
});
it('should hide navigation when only one slide', function () {
scope.slides = [{active:false,content:'one'}];
scope.$apply();
elm = $compile(
'' +
'
' +
'{{slide.content}}' +
'
' +
'
'
)(scope);
var indicators = elm.find('ol.carousel-indicators > li');
expect(indicators.length).toBe(0);
var navNext = elm.find('a.right');
expect(navNext.length).toBe(0);
var navPrev = elm.find('a.left');
expect(navPrev.length).toBe(0);
});
it('should disable prev button when slide index is 0 and noWrap is truthy', function() {
scope.$apply();
var $scope = elm.isolateScope();
$scope.noWrap = function() {return true;};
$scope.isPrevDisabled();
scope.$apply();
var navPrev = elm.find('a.left');
expect(navPrev.hasClass('disabled')).toBe(true);
});
it('should disable next button when last slide is active and noWrap is truthy', function() {
scope.slides = [
{content: 'one', index: 0},
{content: 'two', index: 1}
];
scope.$apply();
var $scope = elm.isolateScope();
$scope.noWrap = function() {return true;};
$scope.next();
$scope.isNextDisabled();
scope.$apply();
var navNext = elm.find('a.right');
expect(navNext.hasClass('disabled')).toBe(true);
});
it('should show navigation when there are 3 slides', function () {
var indicators = elm.find('ol.carousel-indicators > li');
expect(indicators.length).not.toBe(0);
var navNext = elm.find('a.right');
expect(navNext.length).not.toBe(0);
var navPrev = elm.find('a.left');
expect(navPrev.length).not.toBe(0);
});
it('should go to next when clicking next button', function() {
var navNext = elm.find('a.right');
testSlideActive(0);
navNext.click();
testSlideActive(1);
navNext.click();
testSlideActive(2);
navNext.click();
testSlideActive(0);
});
it('should go to prev when clicking prev button', function() {
var navPrev = elm.find('a.left');
testSlideActive(0);
navPrev.click();
testSlideActive(2);
navPrev.click();
testSlideActive(1);
navPrev.click();
testSlideActive(0);
});
it('should select a slide when clicking on slide indicators', function () {
var indicators = elm.find('ol.carousel-indicators > li');
indicators.eq(1).click();
testSlideActive(1);
});
it('shouldnt go forward if interval is NaN or negative or has no slides', function() {
testSlideActive(0);
var previousInterval = scope.interval;
scope.$apply('interval = -1');
$interval.flush(previousInterval);
testSlideActive(0);
scope.$apply('interval = 1000');
$interval.flush(1000);
testSlideActive(1);
scope.$apply('interval = false');
$interval.flush(1000);
testSlideActive(1);
scope.$apply('interval = 1000');
$interval.flush(1000);
testSlideActive(2);
scope.$apply('slides = []');
$interval.flush(1000);
testSlideActive(2);
});
it('should bind the content to slides', function() {
var contents = elm.find('div.item [ng-transclude]');
expect(contents.length).toBe(3);
expect(contents.eq(0).text()).toBe('one');
expect(contents.eq(1).text()).toBe('two');
expect(contents.eq(2).text()).toBe('three');
scope.$apply(function() {
scope.slides[0].content = 'what';
scope.slides[1].content = 'no';
scope.slides[2].content = 'maybe';
});
expect(contents.eq(0).text()).toBe('what');
expect(contents.eq(1).text()).toBe('no');
expect(contents.eq(2).text()).toBe('maybe');
});
it('should be playing by default and cycle through slides', function() {
testSlideActive(0);
$interval.flush(scope.interval);
testSlideActive(1);
$interval.flush(scope.interval);
testSlideActive(2);
$interval.flush(scope.interval);
testSlideActive(0);
});
it('should pause and play on mouseover', function() {
testSlideActive(0);
$interval.flush(scope.interval);
testSlideActive(1);
elm.trigger('mouseenter');
testSlideActive(1);
$interval.flush(scope.interval);
testSlideActive(1);
elm.trigger('mouseleave');
$interval.flush(scope.interval);
testSlideActive(2);
});
it('should not pause on mouseover if noPause', function() {
scope.$apply('nopause = true');
testSlideActive(0);
elm.trigger('mouseenter');
$interval.flush(scope.interval);
testSlideActive(1);
elm.trigger('mouseleave');
$interval.flush(scope.interval);
testSlideActive(2);
});
it('should remove slide from dom and change active slide', function() {
scope.$apply('active = 2');
testSlideActive(2);
scope.$apply('slides.splice(2,1)');
$timeout.flush(0);
expect(elm.find('div.item').length).toBe(2);
testSlideActive(1);
$interval.flush(scope.interval);
testSlideActive(0);
scope.$apply('slides.splice(1,1)');
$timeout.flush(0);
expect(elm.find('div.item').length).toBe(1);
testSlideActive(0);
});
it('should change dom when you reassign ng-repeat slides array', function() {
scope.slides = [
{content:'new1', index: 4},
{content:'new2', index: 5},
{content:'new3', index: 6}
];
scope.$apply();
var contents = elm.find('div.item [ng-transclude]');
expect(contents.length).toBe(3);
expect(contents.eq(0).text()).toBe('new1');
expect(contents.eq(1).text()).toBe('new2');
expect(contents.eq(2).text()).toBe('new3');
});
it('should not change if next is clicked while transitioning', function() {
var carouselScope = elm.children().scope();
var next = elm.find('a.right');
testSlideActive(0);
carouselScope.$currentTransition = true;
next.click();
testSlideActive(0);
carouselScope.$currentTransition = null;
next.click();
testSlideActive(1);
});
it('should buffer the slides if transition is clicked and only transition to the last requested', function() {
var carouselScope = elm.children().scope();
testSlideActive(0);
carouselScope.$currentTransition = null;
carouselScope.select(carouselScope.slides[1]);
$animate.flush();
testSlideActive(1);
carouselScope.$currentTransition = true;
carouselScope.select(carouselScope.slides[2]);
scope.$apply();
testSlideActive(1);
carouselScope.select(carouselScope.slides[0]);
scope.$apply();
testSlideActive(1);
carouselScope.$currentTransition = null;
$interval.flush(scope.interval);
$animate.flush();
testSlideActive(2);
$interval.flush(scope.interval);
$animate.flush();
testSlideActive(0);
});
it('issue 1414 - should not continue running timers after scope is destroyed', function() {
testSlideActive(0);
$interval.flush(scope.interval);
testSlideActive(1);
$interval.flush(scope.interval);
testSlideActive(2);
$interval.flush(scope.interval);
testSlideActive(0);
spyOn($interval, 'cancel').and.callThrough();
scope.$destroy();
expect($interval.cancel).toHaveBeenCalled();
});
it('issue 4390 - should reset the currentTransition if there are no slides', function() {
var carouselScope = elm.children().scope();
var next = elm.find('a.right');
scope.slides = [
{content:'new1', index: 1},
{content:'new2', index: 2},
{content:'new3', index: 3}
];
scope.$apply();
testSlideActive(0);
carouselScope.$currentTransition = true;
scope.slides = [];
scope.$apply();
expect(carouselScope.$currentTransition).toBe(null);
});
});
describe('slide order', function() {
var elm, scope;
beforeEach(function() {
scope = $rootScope.$new();
scope.slides = [
{content: 'one', id: 3},
{content: 'two', id: 1},
{content: 'three', id: 2}
];
elm = $compile(
'' +
'
' +
'{{slide.content}}' +
'
' +
'
'
)(scope);
scope.$apply();
});
function testSlideActive(slideIndex) {
for (var i = 0; i < scope.slides.length; i++) {
if (i === slideIndex) {
expect(scope.active).toBe(scope.slides[i].id);
} else {
expect(scope.active).not.toBe(scope.slides[i].id);
}
}
}
it('should change dom when the order of the slides changes', function() {
scope.slides[0].id = 3;
scope.slides[1].id = 2;
scope.slides[2].id = 1;
scope.$apply();
var contents = elm.find('div.item [ng-transclude]');
expect(contents.length).toBe(3);
expect(contents.eq(0).text()).toBe('three');
expect(contents.eq(1).text()).toBe('two');
expect(contents.eq(2).text()).toBe('one');
});
it('should select next after order change', function() {
testSlideActive(1);
var next = elm.find('a.right');
next.click();
testSlideActive(2);
});
it('should select prev after order change', function() {
testSlideActive(1);
var prev = elm.find('a.left');
prev.click();
testSlideActive(0);
});
it('should add slide in the specified position', function() {
testSlideActive(1);
scope.slides[2].id = 4;
scope.slides.push({content:'four', id: 5});
scope.$apply();
var contents = elm.find('div.item [ng-transclude]');
expect(contents.length).toBe(4);
expect(contents.eq(0).text()).toBe('two');
expect(contents.eq(1).text()).toBe('one');
expect(contents.eq(2).text()).toBe('three');
expect(contents.eq(3).text()).toBe('four');
});
it('should remove slide after order change', function() {
testSlideActive(1);
scope.slides.splice(1, 1);
scope.$apply();
var contents = elm.find('div.item [ng-transclude]');
expect(contents.length).toBe(2);
expect(contents.eq(0).text()).toBe('three');
expect(contents.eq(1).text()).toBe('one');
});
});
describe('controller', function() {
var scope, ctrl;
//create an array of slides and add to the scope
var slides = [
{'content': 1, index: 0},
{'content': 2, index: 1},
{'content': 3, index: 2},
{'content': 4, index: 3}
];
beforeEach(function() {
scope = $rootScope.$new();
scope.noWrap = angular.noop;
ctrl = $controller('UibCarouselController', {$scope: scope, $element: angular.element('')});
for (var i = 0; i < slides.length; i++) {
ctrl.addSlide(slides[i]);
}
});
it('should set first slide to active = true and the rest to false', function() {
angular.forEach(ctrl.slides, function(slide, i) {
if (i !== 0) {
expect(slide.slide.active).not.toBe(true);
} else {
expect(slide.slide.active).toBe(true);
}
});
});
it('should add a new slide and not change the active slide', function() {
var newSlide = {active: false, index: 4};
expect(ctrl.slides.length).toBe(4);
ctrl.addSlide(newSlide);
expect(ctrl.slides.length).toBe(5);
expect(ctrl.slides[4].slide.active).toBe(false);
expect(ctrl.slides[0].slide.active).toBe(true);
});
it('should remove slide and change active slide if needed', function() {
expect(ctrl.slides.length).toBe(4);
ctrl.removeSlide(ctrl.slides[0].slide);
$timeout.flush(0);
expect(ctrl.slides.length).toBe(3);
expect(scope.active).toBe(1);
ctrl.select(ctrl.slides[2]);
ctrl.removeSlide(ctrl.slides[2].slide);
$timeout.flush(0);
expect(ctrl.slides.length).toBe(2);
expect(scope.active).toBe(2);
ctrl.removeSlide(ctrl.slides[0].slide);
$timeout.flush(0);
expect(ctrl.slides.length).toBe(1);
expect(scope.active).toBe(1);
});
it('issue 1414 - should not continue running timers after scope is destroyed', function() {
spyOn(scope, 'next');
scope.interval = 2000;
scope.$digest();
$interval.flush(scope.interval);
expect(scope.next.calls.count()).toBe(1);
scope.$destroy();
$interval.flush(scope.interval);
expect(scope.next.calls.count()).toBe(1);
});
it('should be exposed in the template', inject(function($templateCache) {
$templateCache.put('uib/template/carousel/carousel.html', '{{carousel.text}}
');
var scope = $rootScope.$new();
var elm = $compile('')(scope);
$rootScope.$digest();
var ctrl = elm.controller('uibCarousel');
expect(ctrl).toBeDefined();
ctrl.text = 'foo';
$rootScope.$digest();
expect(elm.html()).toBe('foo
');
}));
});
it('should expose a custom model in the carousel slide', function() {
var scope = $rootScope.$new();
scope.slides = [
{active:false,content:'one'},
{active:false,content:'two'},
{active:false,content:'three'}
];
var elm = $compile(
'' +
'
' +
'{{slide.content}}' +
'
' +
'
'
)(scope);
$rootScope.$digest();
var ctrl = elm.controller('uibCarousel');
expect(angular.equals(ctrl.slides.map(function(slide) {
return slide.slide.actual;
}), scope.slides)).toBe(true);
});
});
================================================
FILE: src/collapse/collapse.js
================================================
angular.module('ui.bootstrap.collapse', [])
.directive('uibCollapse', ['$animate', '$q', '$parse', '$injector', function($animate, $q, $parse, $injector) {
var $animateCss = $injector.has('$animateCss') ? $injector.get('$animateCss') : null;
return {
link: function(scope, element, attrs) {
var expandingExpr = $parse(attrs.expanding),
expandedExpr = $parse(attrs.expanded),
collapsingExpr = $parse(attrs.collapsing),
collapsedExpr = $parse(attrs.collapsed),
horizontal = false,
css = {},
cssTo = {};
init();
function init() {
horizontal = !!('horizontal' in attrs);
if (horizontal) {
css = {
width: ''
};
cssTo = {width: '0'};
} else {
css = {
height: ''
};
cssTo = {height: '0'};
}
if (!scope.$eval(attrs.uibCollapse)) {
element.addClass('in')
.addClass('collapse')
.attr('aria-expanded', true)
.attr('aria-hidden', false)
.css(css);
}
}
function getScrollFromElement(element) {
if (horizontal) {
return {width: element.scrollWidth + 'px'};
}
return {height: element.scrollHeight + 'px'};
}
function expand() {
if (element.hasClass('collapse') && element.hasClass('in')) {
return;
}
$q.resolve(expandingExpr(scope))
.then(function() {
element.removeClass('collapse')
.addClass('collapsing')
.attr('aria-expanded', true)
.attr('aria-hidden', false);
if ($animateCss) {
$animateCss(element, {
addClass: 'in',
easing: 'ease',
css: {
overflow: 'hidden'
},
to: getScrollFromElement(element[0])
}).start()['finally'](expandDone);
} else {
$animate.addClass(element, 'in', {
css: {
overflow: 'hidden'
},
to: getScrollFromElement(element[0])
}).then(expandDone);
}
}, angular.noop);
}
function expandDone() {
element.removeClass('collapsing')
.addClass('collapse')
.css(css);
expandedExpr(scope);
}
function collapse() {
if (!element.hasClass('collapse') && !element.hasClass('in')) {
return collapseDone();
}
$q.resolve(collapsingExpr(scope))
.then(function() {
element
// IMPORTANT: The width must be set before adding "collapsing" class.
// Otherwise, the browser attempts to animate from width 0 (in
// collapsing class) to the given width here.
.css(getScrollFromElement(element[0]))
// initially all panel collapse have the collapse class, this removal
// prevents the animation from jumping to collapsed state
.removeClass('collapse')
.addClass('collapsing')
.attr('aria-expanded', false)
.attr('aria-hidden', true);
if ($animateCss) {
$animateCss(element, {
removeClass: 'in',
to: cssTo
}).start()['finally'](collapseDone);
} else {
$animate.removeClass(element, 'in', {
to: cssTo
}).then(collapseDone);
}
}, angular.noop);
}
function collapseDone() {
element.css(cssTo); // Required so that collapse works when animation is disabled
element.removeClass('collapsing')
.addClass('collapse');
collapsedExpr(scope);
}
scope.$watch(attrs.uibCollapse, function(shouldCollapse) {
if (shouldCollapse) {
collapse();
} else {
expand();
}
});
}
};
}]);
================================================
FILE: src/collapse/docs/demo.html
================================================
Resize window to less than 768 pixels to display mobile menu toggle button.
================================================
FILE: src/collapse/docs/demo.js
================================================
angular.module('ui.bootstrap.demo').controller('CollapseDemoCtrl', function ($scope) {
$scope.isNavCollapsed = true;
$scope.isCollapsed = false;
$scope.isCollapsedHorizontal = false;
});
================================================
FILE: src/collapse/docs/readme.md
================================================
**uib-collapse** provides a simple way to hide and show an element with a css transition
### uib-collapse settings
* `collapsed()`
$ -
An optional expression called after the element finished collapsing.
* `collapsing()`
$ -
An optional expression called before the element begins collapsing.
If the expression returns a promise, animation won't start until the promise resolves.
If the returned promise is rejected, collapsing will be cancelled.
* `expanded()`
$ -
An optional expression called after the element finished expanding.
* `expanding()`
$ -
An optional expression called before the element begins expanding.
If the expression returns a promise, animation won't start until the promise resolves.
If the returned promise is rejected, expanding will be cancelled.
* `uib-collapse`
$
_(Default: `false`)_ -
Whether the element should be collapsed or not.
* `horizontal`
$ -
An optional attribute that permit to collapse horizontally.
### Known Issues
When using the `horizontal` attribute with this directive, CSS can reflow as the collapse element goes from `0px` to its desired end width, which can result in height changes. This can cause animations to not appear to run. The best way around this is to set a fixed height via CSS on the horizontal collapse element so that this situation does not occur, and so the animation can run as expected.
================================================
FILE: src/collapse/index.js
================================================
require('./collapse');
var MODULE_NAME = 'ui.bootstrap.module.collapse';
angular.module(MODULE_NAME, ['ui.bootstrap.collapse']);
module.exports = MODULE_NAME;
================================================
FILE: src/collapse/test/collapse.spec.js
================================================
describe('collapse directive', function() {
var element, compileFn, scope, $compile, $animate, $q;
beforeEach(module('ui.bootstrap.collapse'));
beforeEach(module('ngAnimateMock'));
beforeEach(inject(function(_$rootScope_, _$compile_, _$animate_, _$q_) {
scope = _$rootScope_;
$compile = _$compile_;
$animate = _$animate_;
$q = _$q_;
}));
beforeEach(function() {
element = angular.element(
''
+ 'Some Content
');
compileFn = $compile(element);
angular.element(document.body).append(element);
});
afterEach(function() {
element.remove();
});
function initCallbacks() {
scope.collapsing = jasmine.createSpy('scope.collapsing');
scope.collapsed = jasmine.createSpy('scope.collapsed');
scope.expanding = jasmine.createSpy('scope.expanding');
scope.expanded = jasmine.createSpy('scope.expanded');
}
function assertCallbacks(expected) {
['collapsing', 'collapsed', 'expanding', 'expanded'].forEach(function(cbName) {
if (expected[cbName]) {
expect(scope[cbName]).toHaveBeenCalled();
} else {
expect(scope[cbName]).not.toHaveBeenCalled();
}
});
}
it('should be hidden on initialization if isCollapsed = true', function() {
initCallbacks();
scope.isCollapsed = true;
compileFn(scope);
scope.$digest();
expect(element.height()).toBe(0);
assertCallbacks({ collapsed: true });
});
it('should not trigger any animation on initialization if isCollapsed = true', function() {
var wrapperFn = function() {
$animate.flush();
};
scope.isCollapsed = true;
compileFn(scope);
scope.$digest();
expect(wrapperFn).toThrowError(/No pending animations ready to be closed or flushed/);
});
it('should collapse if isCollapsed = true on subsequent use', function() {
scope.isCollapsed = false;
compileFn(scope);
scope.$digest();
initCallbacks();
scope.isCollapsed = true;
scope.$digest();
$animate.flush();
expect(element.height()).toBe(0);
assertCallbacks({ collapsing: true, collapsed: true });
});
it('should show after toggled from collapsed', function() {
initCallbacks();
scope.isCollapsed = true;
compileFn(scope);
scope.$digest();
expect(element.height()).toBe(0);
assertCallbacks({ collapsed: true });
scope.collapsed.calls.reset();
scope.isCollapsed = false;
scope.$digest();
$animate.flush();
expect(element.height()).not.toBe(0);
assertCallbacks({ expanding: true, expanded: true });
});
it('should not trigger any animation on initialization if isCollapsed = false', function() {
var wrapperFn = function() {
$animate.flush();
};
scope.isCollapsed = false;
compileFn(scope);
scope.$digest();
expect(wrapperFn).toThrowError(/No pending animations ready to be closed or flushed/);
});
it('should expand if isCollapsed = false on subsequent use', function() {
scope.isCollapsed = false;
compileFn(scope);
scope.$digest();
scope.isCollapsed = true;
scope.$digest();
$animate.flush();
initCallbacks();
scope.isCollapsed = false;
scope.$digest();
$animate.flush();
expect(element.height()).not.toBe(0);
assertCallbacks({ expanding: true, expanded: true });
});
it('should collapse if isCollapsed = true on subsequent uses', function() {
scope.isCollapsed = false;
compileFn(scope);
scope.$digest();
scope.isCollapsed = true;
scope.$digest();
$animate.flush();
scope.isCollapsed = false;
scope.$digest();
$animate.flush();
initCallbacks();
scope.isCollapsed = true;
scope.$digest();
$animate.flush();
expect(element.height()).toBe(0);
assertCallbacks({ collapsing: true, collapsed: true });
});
it('should change aria-expanded attribute', function() {
scope.isCollapsed = false;
compileFn(scope);
scope.$digest();
expect(element.attr('aria-expanded')).toBe('true');
scope.isCollapsed = true;
scope.$digest();
$animate.flush();
expect(element.attr('aria-expanded')).toBe('false');
});
it('should change aria-hidden attribute', function() {
scope.isCollapsed = false;
compileFn(scope);
scope.$digest();
expect(element.attr('aria-hidden')).toBe('false');
scope.isCollapsed = true;
scope.$digest();
$animate.flush();
expect(element.attr('aria-hidden')).toBe('true');
});
describe('dynamic content', function() {
var element;
beforeEach(function() {
element = angular.element('Initial content
Additional content
');
$compile(element)(scope);
angular.element(document.body).append(element);
});
afterEach(function() {
element.remove();
});
it('should grow accordingly when content size inside collapse increases', function() {
scope.exp = false;
scope.isCollapsed = false;
scope.$digest();
var collapseHeight = element.height();
scope.exp = true;
scope.$digest();
expect(element.height()).toBeGreaterThan(collapseHeight);
});
it('should shrink accordingly when content size inside collapse decreases', function() {
scope.exp = true;
scope.isCollapsed = false;
scope.$digest();
var collapseHeight = element.height();
scope.exp = false;
scope.$digest();
expect(element.height()).toBeLessThan(collapseHeight);
});
});
describe('expanding callback returning a promise', function() {
var defer, collapsedHeight;
beforeEach(function() {
defer = $q.defer();
scope.isCollapsed = true;
scope.expanding = function() {
return defer.promise;
};
compileFn(scope);
scope.$digest();
collapsedHeight = element.height();
// set flag to expand ...
scope.isCollapsed = false;
scope.$digest();
// ... shouldn't expand yet ...
expect(element.attr('aria-expanded')).not.toBe('true');
expect(element.height()).toBe(collapsedHeight);
});
it('should wait for it to resolve before animating', function() {
defer.resolve();
// should now expand
scope.$digest();
$animate.flush();
expect(element.attr('aria-expanded')).toBe('true');
expect(element.height()).toBeGreaterThan(collapsedHeight);
});
it('should not animate if it rejects', function() {
defer.reject();
// should NOT expand
scope.$digest();
expect(element.attr('aria-expanded')).not.toBe('true');
expect(element.height()).toBe(collapsedHeight);
});
});
describe('collapsing callback returning a promise', function() {
var defer, expandedHeight;
beforeEach(function() {
defer = $q.defer();
scope.isCollapsed = false;
scope.collapsing = function() {
return defer.promise;
};
compileFn(scope);
scope.$digest();
expandedHeight = element.height();
// set flag to collapse ...
scope.isCollapsed = true;
scope.$digest();
// ... but it shouldn't collapse yet ...
expect(element.attr('aria-expanded')).not.toBe('false');
expect(element.height()).toBe(expandedHeight);
});
it('should wait for it to resolve before animating', function() {
defer.resolve();
// should now collapse
scope.$digest();
$animate.flush();
expect(element.attr('aria-expanded')).toBe('false');
expect(element.height()).toBeLessThan(expandedHeight);
});
it('should not animate if it rejects', function() {
defer.reject();
// should NOT collapse
scope.$digest();
expect(element.attr('aria-expanded')).not.toBe('false');
expect(element.height()).toBe(expandedHeight);
});
});
});
================================================
FILE: src/collapse/test/collapseHorizontally.spec.js
================================================
describe('collapse directive', function() {
var elementH, compileFnH, scope, $compile, $animate, $q;
beforeEach(module('ui.bootstrap.collapse'));
beforeEach(module('ngAnimateMock'));
beforeEach(inject(function(_$rootScope_, _$compile_, _$animate_, _$q_) {
scope = _$rootScope_;
$compile = _$compile_;
$animate = _$animate_;
$q = _$q_;
}));
beforeEach(function() {
elementH = angular.element(
''
+ 'Some Content
');
compileFnH = $compile(elementH);
angular.element(document.body).append(elementH);
});
afterEach(function() {
elementH.remove();
});
function initCallbacks() {
scope.collapsing = jasmine.createSpy('scope.collapsing');
scope.collapsed = jasmine.createSpy('scope.collapsed');
scope.expanding = jasmine.createSpy('scope.expanding');
scope.expanded = jasmine.createSpy('scope.expanded');
}
function assertCallbacks(expected) {
['collapsing', 'collapsed', 'expanding', 'expanded'].forEach(function(cbName) {
if (expected[cbName]) {
expect(scope[cbName]).toHaveBeenCalled();
} else {
expect(scope[cbName]).not.toHaveBeenCalled();
}
});
}
it('should be hidden on initialization if isCollapsed = true', function() {
initCallbacks();
scope.isCollapsed = true;
compileFnH(scope);
scope.$digest();
expect(elementH.width()).toBe(0);
assertCallbacks({ collapsed: true });
});
it('should not trigger any animation on initialization if isCollapsed = true', function() {
var wrapperFn = function() {
$animate.flush();
};
scope.isCollapsed = true;
compileFnH(scope);
scope.$digest();
expect(wrapperFn).toThrowError(/No pending animations ready to be closed or flushed/);
});
it('should collapse if isCollapsed = true on subsequent use', function() {
scope.isCollapsed = false;
compileFnH(scope);
scope.$digest();
initCallbacks();
scope.isCollapsed = true;
scope.$digest();
$animate.flush();
expect(elementH.width()).toBe(0);
assertCallbacks({ collapsing: true, collapsed: true });
});
it('should show after toggled from collapsed', function() {
initCallbacks();
scope.isCollapsed = true;
compileFnH(scope);
scope.$digest();
expect(elementH.width()).toBe(0);
assertCallbacks({ collapsed: true });
scope.collapsed.calls.reset();
scope.isCollapsed = false;
scope.$digest();
$animate.flush();
expect(elementH.width()).not.toBe(0);
assertCallbacks({ expanding: true, expanded: true });
});
it('should not trigger any animation on initialization if isCollapsed = false', function() {
var wrapperFn = function() {
$animate.flush();
};
scope.isCollapsed = false;
compileFnH(scope);
scope.$digest();
expect(wrapperFn).toThrowError(/No pending animations ready to be closed or flushed/);
});
it('should expand if isCollapsed = false on subsequent use', function() {
scope.isCollapsed = false;
compileFnH(scope);
scope.$digest();
scope.isCollapsed = true;
scope.$digest();
$animate.flush();
initCallbacks();
scope.isCollapsed = false;
scope.$digest();
$animate.flush();
expect(elementH.width()).not.toBe(0);
assertCallbacks({ expanding: true, expanded: true });
});
it('should collapse if isCollapsed = true on subsequent uses', function() {
scope.isCollapsed = false;
compileFnH(scope);
scope.$digest();
scope.isCollapsed = true;
scope.$digest();
$animate.flush();
scope.isCollapsed = false;
scope.$digest();
$animate.flush();
initCallbacks();
scope.isCollapsed = true;
scope.$digest();
$animate.flush();
expect(elementH.width()).toBe(0);
assertCallbacks({ collapsing: true, collapsed: true });
});
it('should change aria-expanded attribute', function() {
scope.isCollapsed = false;
compileFnH(scope);
scope.$digest();
expect(elementH.attr('aria-expanded')).toBe('true');
scope.isCollapsed = true;
scope.$digest();
$animate.flush();
expect(elementH.attr('aria-expanded')).toBe('false');
});
it('should change aria-hidden attribute', function() {
scope.isCollapsed = false;
compileFnH(scope);
scope.$digest();
expect(elementH.attr('aria-hidden')).toBe('false');
scope.isCollapsed = true;
scope.$digest();
$animate.flush();
expect(elementH.attr('aria-hidden')).toBe('true');
});
describe('expanding callback returning a promise', function() {
var defer, collapsedWidth;
beforeEach(function() {
defer = $q.defer();
scope.isCollapsed = true;
scope.expanding = function() {
return defer.promise;
};
compileFnH(scope);
scope.$digest();
collapsedWidth = elementH.width();
// set flag to expand ...
scope.isCollapsed = false;
scope.$digest();
// ... shouldn't expand yet ...
expect(elementH.attr('aria-expanded')).not.toBe('true');
expect(elementH.width()).toBe(collapsedWidth);
});
it('should wait for it to resolve before animating', function() {
defer.resolve();
// should now expand
scope.$digest();
$animate.flush();
expect(elementH.attr('aria-expanded')).toBe('true');
expect(elementH.width()).toBeGreaterThan(collapsedWidth);
});
it('should not animate if it rejects', function() {
defer.reject();
// should NOT expand
scope.$digest();
expect(elementH.attr('aria-expanded')).not.toBe('true');
expect(elementH.width()).toBe(collapsedWidth);
});
});
describe('collapsing callback returning a promise', function() {
var defer, expandedWidth;
beforeEach(function() {
defer = $q.defer();
scope.isCollapsed = false;
scope.collapsing = function() {
return defer.promise;
};
compileFnH(scope);
scope.$digest();
expandedWidth = elementH.width();
// set flag to collapse ...
scope.isCollapsed = true;
scope.$digest();
// ... but it shouldn't collapse yet ...
expect(elementH.attr('aria-expanded')).not.toBe('false');
expect(elementH.width()).toBe(expandedWidth);
});
it('should wait for it to resolve before animating', function() {
defer.resolve();
// should now collapse
scope.$digest();
$animate.flush();
expect(elementH.attr('aria-expanded')).toBe('false');
expect(elementH.width()).toBeLessThan(expandedWidth);
});
it('should not animate if it rejects', function() {
defer.reject();
// should NOT collapse
scope.$digest();
expect(elementH.attr('aria-expanded')).not.toBe('false');
expect(elementH.width()).toBe(expandedWidth);
});
});
});
================================================
FILE: src/dateparser/dateparser.js
================================================
angular.module('ui.bootstrap.dateparser', [])
.service('uibDateParser', ['$log', '$locale', 'dateFilter', 'orderByFilter', 'filterFilter', function($log, $locale, dateFilter, orderByFilter, filterFilter) {
// Pulled from https://github.com/mbostock/d3/blob/master/src/format/requote.js
var SPECIAL_CHARACTERS_REGEXP = /[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g;
var localeId;
var formatCodeToRegex;
this.init = function() {
localeId = $locale.id;
this.parsers = {};
this.formatters = {};
formatCodeToRegex = [
{
key: 'yyyy',
regex: '\\d{4}',
apply: function(value) { this.year = +value; },
formatter: function(date) {
var _date = new Date();
_date.setFullYear(Math.abs(date.getFullYear()));
return dateFilter(_date, 'yyyy');
}
},
{
key: 'yy',
regex: '\\d{2}',
apply: function(value) { value = +value; this.year = value < 69 ? value + 2000 : value + 1900; },
formatter: function(date) {
var _date = new Date();
_date.setFullYear(Math.abs(date.getFullYear()));
return dateFilter(_date, 'yy');
}
},
{
key: 'y',
regex: '\\d{1,4}',
apply: function(value) { this.year = +value; },
formatter: function(date) {
var _date = new Date();
_date.setFullYear(Math.abs(date.getFullYear()));
return dateFilter(_date, 'y');
}
},
{
key: 'M!',
regex: '0?[1-9]|1[0-2]',
apply: function(value) { this.month = value - 1; },
formatter: function(date) {
var value = date.getMonth();
if (/^[0-9]$/.test(value)) {
return dateFilter(date, 'MM');
}
return dateFilter(date, 'M');
}
},
{
key: 'MMMM',
regex: $locale.DATETIME_FORMATS.MONTH.join('|'),
apply: function(value) { this.month = $locale.DATETIME_FORMATS.MONTH.indexOf(value); },
formatter: function(date) { return dateFilter(date, 'MMMM'); }
},
{
key: 'MMM',
regex: $locale.DATETIME_FORMATS.SHORTMONTH.join('|'),
apply: function(value) { this.month = $locale.DATETIME_FORMATS.SHORTMONTH.indexOf(value); },
formatter: function(date) { return dateFilter(date, 'MMM'); }
},
{
key: 'MM',
regex: '0[1-9]|1[0-2]',
apply: function(value) { this.month = value - 1; },
formatter: function(date) { return dateFilter(date, 'MM'); }
},
{
key: 'M',
regex: '[1-9]|1[0-2]',
apply: function(value) { this.month = value - 1; },
formatter: function(date) { return dateFilter(date, 'M'); }
},
{
key: 'd!',
regex: '[0-2]?[0-9]{1}|3[0-1]{1}',
apply: function(value) { this.date = +value; },
formatter: function(date) {
var value = date.getDate();
if (/^[1-9]$/.test(value)) {
return dateFilter(date, 'dd');
}
return dateFilter(date, 'd');
}
},
{
key: 'dd',
regex: '[0-2][0-9]{1}|3[0-1]{1}',
apply: function(value) { this.date = +value; },
formatter: function(date) { return dateFilter(date, 'dd'); }
},
{
key: 'd',
regex: '[1-2]?[0-9]{1}|3[0-1]{1}',
apply: function(value) { this.date = +value; },
formatter: function(date) { return dateFilter(date, 'd'); }
},
{
key: 'EEEE',
regex: $locale.DATETIME_FORMATS.DAY.join('|'),
formatter: function(date) { return dateFilter(date, 'EEEE'); }
},
{
key: 'EEE',
regex: $locale.DATETIME_FORMATS.SHORTDAY.join('|'),
formatter: function(date) { return dateFilter(date, 'EEE'); }
},
{
key: 'HH',
regex: '(?:0|1)[0-9]|2[0-3]',
apply: function(value) { this.hours = +value; },
formatter: function(date) { return dateFilter(date, 'HH'); }
},
{
key: 'hh',
regex: '0[0-9]|1[0-2]',
apply: function(value) { this.hours = +value; },
formatter: function(date) { return dateFilter(date, 'hh'); }
},
{
key: 'H',
regex: '1?[0-9]|2[0-3]',
apply: function(value) { this.hours = +value; },
formatter: function(date) { return dateFilter(date, 'H'); }
},
{
key: 'h',
regex: '[0-9]|1[0-2]',
apply: function(value) { this.hours = +value; },
formatter: function(date) { return dateFilter(date, 'h'); }
},
{
key: 'mm',
regex: '[0-5][0-9]',
apply: function(value) { this.minutes = +value; },
formatter: function(date) { return dateFilter(date, 'mm'); }
},
{
key: 'm',
regex: '[0-9]|[1-5][0-9]',
apply: function(value) { this.minutes = +value; },
formatter: function(date) { return dateFilter(date, 'm'); }
},
{
key: 'sss',
regex: '[0-9][0-9][0-9]',
apply: function(value) { this.milliseconds = +value; },
formatter: function(date) { return dateFilter(date, 'sss'); }
},
{
key: 'ss',
regex: '[0-5][0-9]',
apply: function(value) { this.seconds = +value; },
formatter: function(date) { return dateFilter(date, 'ss'); }
},
{
key: 's',
regex: '[0-9]|[1-5][0-9]',
apply: function(value) { this.seconds = +value; },
formatter: function(date) { return dateFilter(date, 's'); }
},
{
key: 'a',
regex: $locale.DATETIME_FORMATS.AMPMS.join('|'),
apply: function(value) {
if (this.hours === 12) {
this.hours = 0;
}
if (value === 'PM') {
this.hours += 12;
}
},
formatter: function(date) { return dateFilter(date, 'a'); }
},
{
key: 'Z',
regex: '[+-]\\d{4}',
apply: function(value) {
var matches = value.match(/([+-])(\d{2})(\d{2})/),
sign = matches[1],
hours = matches[2],
minutes = matches[3];
this.hours += toInt(sign + hours);
this.minutes += toInt(sign + minutes);
},
formatter: function(date) {
return dateFilter(date, 'Z');
}
},
{
key: 'ww',
regex: '[0-4][0-9]|5[0-3]',
formatter: function(date) { return dateFilter(date, 'ww'); }
},
{
key: 'w',
regex: '[0-9]|[1-4][0-9]|5[0-3]',
formatter: function(date) { return dateFilter(date, 'w'); }
},
{
key: 'GGGG',
regex: $locale.DATETIME_FORMATS.ERANAMES.join('|').replace(/\s/g, '\\s'),
formatter: function(date) { return dateFilter(date, 'GGGG'); }
},
{
key: 'GGG',
regex: $locale.DATETIME_FORMATS.ERAS.join('|'),
formatter: function(date) { return dateFilter(date, 'GGG'); }
},
{
key: 'GG',
regex: $locale.DATETIME_FORMATS.ERAS.join('|'),
formatter: function(date) { return dateFilter(date, 'GG'); }
},
{
key: 'G',
regex: $locale.DATETIME_FORMATS.ERAS.join('|'),
formatter: function(date) { return dateFilter(date, 'G'); }
}
];
if (angular.version.major >= 1 && angular.version.minor > 4) {
formatCodeToRegex.push({
key: 'LLLL',
regex: $locale.DATETIME_FORMATS.STANDALONEMONTH.join('|'),
apply: function(value) { this.month = $locale.DATETIME_FORMATS.STANDALONEMONTH.indexOf(value); },
formatter: function(date) { return dateFilter(date, 'LLLL'); }
});
}
};
this.init();
function getFormatCodeToRegex(key) {
return filterFilter(formatCodeToRegex, {key: key}, true)[0];
}
this.getParser = function (key) {
var f = getFormatCodeToRegex(key);
return f && f.apply || null;
};
this.overrideParser = function (key, parser) {
var f = getFormatCodeToRegex(key);
if (f && angular.isFunction(parser)) {
this.parsers = {};
f.apply = parser;
}
}.bind(this);
function createParser(format) {
var map = [], regex = format.split('');
// check for literal values
var quoteIndex = format.indexOf('\'');
if (quoteIndex > -1) {
var inLiteral = false;
format = format.split('');
for (var i = quoteIndex; i < format.length; i++) {
if (inLiteral) {
if (format[i] === '\'') {
if (i + 1 < format.length && format[i+1] === '\'') { // escaped single quote
format[i+1] = '$';
regex[i+1] = '';
} else { // end of literal
regex[i] = '';
inLiteral = false;
}
}
format[i] = '$';
} else {
if (format[i] === '\'') { // start of literal
format[i] = '$';
regex[i] = '';
inLiteral = true;
}
}
}
format = format.join('');
}
angular.forEach(formatCodeToRegex, function(data) {
var index = format.indexOf(data.key);
if (index > -1) {
format = format.split('');
regex[index] = '(' + data.regex + ')';
format[index] = '$'; // Custom symbol to define consumed part of format
for (var i = index + 1, n = index + data.key.length; i < n; i++) {
regex[i] = '';
format[i] = '$';
}
format = format.join('');
map.push({
index: index,
key: data.key,
apply: data.apply,
matcher: data.regex
});
}
});
return {
regex: new RegExp('^' + regex.join('') + '$'),
map: orderByFilter(map, 'index')
};
}
function createFormatter(format) {
var formatters = [];
var i = 0;
var formatter, literalIdx;
while (i < format.length) {
if (angular.isNumber(literalIdx)) {
if (format.charAt(i) === '\'') {
if (i + 1 >= format.length || format.charAt(i + 1) !== '\'') {
formatters.push(constructLiteralFormatter(format, literalIdx, i));
literalIdx = null;
}
} else if (i === format.length) {
while (literalIdx < format.length) {
formatter = constructFormatterFromIdx(format, literalIdx);
formatters.push(formatter);
literalIdx = formatter.endIdx;
}
}
i++;
continue;
}
if (format.charAt(i) === '\'') {
literalIdx = i;
i++;
continue;
}
formatter = constructFormatterFromIdx(format, i);
formatters.push(formatter.parser);
i = formatter.endIdx;
}
return formatters;
}
function constructLiteralFormatter(format, literalIdx, endIdx) {
return function() {
return format.substr(literalIdx + 1, endIdx - literalIdx - 1);
};
}
function constructFormatterFromIdx(format, i) {
var currentPosStr = format.substr(i);
for (var j = 0; j < formatCodeToRegex.length; j++) {
if (new RegExp('^' + formatCodeToRegex[j].key).test(currentPosStr)) {
var data = formatCodeToRegex[j];
return {
endIdx: i + data.key.length,
parser: data.formatter
};
}
}
return {
endIdx: i + 1,
parser: function() {
return currentPosStr.charAt(0);
}
};
}
this.filter = function(date, format) {
if (!angular.isDate(date) || isNaN(date) || !format) {
return '';
}
format = $locale.DATETIME_FORMATS[format] || format;
if ($locale.id !== localeId) {
this.init();
}
if (!this.formatters[format]) {
this.formatters[format] = createFormatter(format);
}
var formatters = this.formatters[format];
return formatters.reduce(function(str, formatter) {
return str + formatter(date);
}, '');
};
this.parse = function(input, format, baseDate) {
if (!angular.isString(input) || !format) {
return input;
}
format = $locale.DATETIME_FORMATS[format] || format;
format = format.replace(SPECIAL_CHARACTERS_REGEXP, '\\$&');
if ($locale.id !== localeId) {
this.init();
}
if (!this.parsers[format]) {
this.parsers[format] = createParser(format, 'apply');
}
var parser = this.parsers[format],
regex = parser.regex,
map = parser.map,
results = input.match(regex),
tzOffset = false;
if (results && results.length) {
var fields, dt;
if (angular.isDate(baseDate) && !isNaN(baseDate.getTime())) {
fields = {
year: baseDate.getFullYear(),
month: baseDate.getMonth(),
date: baseDate.getDate(),
hours: baseDate.getHours(),
minutes: baseDate.getMinutes(),
seconds: baseDate.getSeconds(),
milliseconds: baseDate.getMilliseconds()
};
} else {
if (baseDate) {
$log.warn('dateparser:', 'baseDate is not a valid date');
}
fields = { year: 1900, month: 0, date: 1, hours: 0, minutes: 0, seconds: 0, milliseconds: 0 };
}
for (var i = 1, n = results.length; i < n; i++) {
var mapper = map[i - 1];
if (mapper.matcher === 'Z') {
tzOffset = true;
}
if (mapper.apply) {
mapper.apply.call(fields, results[i]);
}
}
var datesetter = tzOffset ? Date.prototype.setUTCFullYear :
Date.prototype.setFullYear;
var timesetter = tzOffset ? Date.prototype.setUTCHours :
Date.prototype.setHours;
if (isValid(fields.year, fields.month, fields.date)) {
if (angular.isDate(baseDate) && !isNaN(baseDate.getTime()) && !tzOffset) {
dt = new Date(baseDate);
datesetter.call(dt, fields.year, fields.month, fields.date);
timesetter.call(dt, fields.hours, fields.minutes,
fields.seconds, fields.milliseconds);
} else {
dt = new Date(0);
datesetter.call(dt, fields.year, fields.month, fields.date);
timesetter.call(dt, fields.hours || 0, fields.minutes || 0,
fields.seconds || 0, fields.milliseconds || 0);
}
}
return dt;
}
};
// Check if date is valid for specific month (and year for February).
// Month: 0 = Jan, 1 = Feb, etc
function isValid(year, month, date) {
if (date < 1) {
return false;
}
if (month === 1 && date > 28) {
return date === 29 && (year % 4 === 0 && year % 100 !== 0 || year % 400 === 0);
}
if (month === 3 || month === 5 || month === 8 || month === 10) {
return date < 31;
}
return true;
}
function toInt(str) {
return parseInt(str, 10);
}
this.toTimezone = toTimezone;
this.fromTimezone = fromTimezone;
this.timezoneToOffset = timezoneToOffset;
this.addDateMinutes = addDateMinutes;
this.convertTimezoneToLocal = convertTimezoneToLocal;
function toTimezone(date, timezone) {
return date && timezone ? convertTimezoneToLocal(date, timezone) : date;
}
function fromTimezone(date, timezone) {
return date && timezone ? convertTimezoneToLocal(date, timezone, true) : date;
}
//https://github.com/angular/angular.js/blob/622c42169699ec07fc6daaa19fe6d224e5d2f70e/src/Angular.js#L1207
function timezoneToOffset(timezone, fallback) {
timezone = timezone.replace(/:/g, '');
var requestedTimezoneOffset = Date.parse('Jan 01, 1970 00:00:00 ' + timezone) / 60000;
return isNaN(requestedTimezoneOffset) ? fallback : requestedTimezoneOffset;
}
function addDateMinutes(date, minutes) {
date = new Date(date.getTime());
date.setMinutes(date.getMinutes() + minutes);
return date;
}
function convertTimezoneToLocal(date, timezone, reverse) {
reverse = reverse ? -1 : 1;
var dateTimezoneOffset = date.getTimezoneOffset();
var timezoneOffset = timezoneToOffset(timezone, dateTimezoneOffset);
return addDateMinutes(date, reverse * (timezoneOffset - dateTimezoneOffset));
}
}]);
================================================
FILE: src/dateparser/docs/README.md
================================================
The `uibDateParser` is what the `uib-datepicker` uses internally to parse the dates. You can use it standalone by injecting the `uibDateParser` service where you need it.
The public API for the dateParser is a single method called `parse`.
Certain format codes support i18n. Check this [guide](https://docs.angularjs.org/guide/i18n) for more information.
### uibDateParser's parse function
##### parameters
* `input`
_(Type: `string`, Example: `2004/Sep/4`)_ -
The input date to parse.
* `format`
_(Type: `string`, Example: `yyyy/MMM/d`)_ -
The format we want to use. Check all the supported formats below.
* `baseDate`
_(Type: `Date`, Example: `new Date()`)_ -
If you want to parse a date but maintain the timezone, you can pass an existing date here.
##### return
* If the specified input matches the format, a new date with the input will be returned, otherwise, it will return undefined.
### uibDateParser's format codes
* `yyyy`
_(Example: `2015`)_ -
Parses a 4 digits year.
* `yy`
_(Example: `15`)_ -
Parses a 2 digits year.
* `y`
_(Example: `15`)_ -
Parses a year with 1, 2, 3, or 4 digits.
* `MMMM`
_(Example: `February`, i18n support)_ -
Parses the full name of a month.
* `MMM`
_(Example: `Feb`, i18n support)_ -
Parses the short name of a month.
* `MM`
_(Example: `12`, Leading 0)_ -
Parses a numeric month.
* `M`
_(Example: `3`)_ -
Parses a numeric month.
* `M!`
_(Example: `3` or `03`)_ -
Parses a numeric month, but allowing an optional leading zero
* `LLLL`
_(Example: `February`, i18n support)_ - Stand-alone month in year (January-December). Requires Angular version 1.5.1 or higher.
* `dd`
_(Example: `05`, Leading 0)_ -
Parses a numeric day.
* `d`
_(Example: `5`)_ -
Parses a numeric day.
* `d!`
_(Example: `3` or `03`)_ -
Parses a numeric day, but allowing an optional leading zero
* `EEEE`
_(Example: `Sunday`, i18n support)_ -
Parses the full name of a day.
* `EEE`
_(Example: `Mon`, i18n support)_ -
Parses the short name of a day.
* `HH`
_(Example: `14`, Leading 0)_ -
Parses a 24 hours time.
* `H`
_(Example: `3`)_ -
Parses a 24 hours time.
* `hh`
_(Example: `11`, Leading 0)_ -
Parses a 12 hours time.
* `h`
_(Example: `3`)_ -
Parses a 12 hours time.
* `mm`
_(Example: `09`, Leading 0)_ -
Parses the minutes.
* `m`
_(Example: `3`)_ -
Parses the minutes.
* `sss`
_(Example: `094`, Leading 0)_ -
Parses the milliseconds.
* `ss`
_(Example: `08`, Leading 0)_ -
Parses the seconds.
* `s`
_(Example: `22`)_ -
Parses the seconds.
* `a`
_(Example: `10AM`)_ -
Parses a 12 hours time with AM/PM.
* `Z`
_(Example: `-0800`)_ -
Parses the timezone offset in a signed 4 digit representation
* `ww`
_(Example: `03`, Leading 0)_ -
Parses the week number
* `w`
_(Example: `03`)_ -
Parses the week number
* `G`, `GG`, `GGG`
_(Example: `AD`)_ -
Parses the era (`AD` or `BC`)
* `GGGG`
_(Example: `Anno Domini`)_ -
Parses the long form of the era (`Anno Domini` or `Before Christ`)
\* The ones marked with `Leading 0`, needs a leading 0 for values less than 10. Exception being milliseconds which needs it for values under 100.
\** It also supports `fullDate|longDate|medium|mediumDate|mediumTime|short|shortDate|shortTime` as the format for parsing.
\*** It supports template literals as a string between the single quote `'` character, i.e. `'The Date is' MM/DD/YYYY`. If one wants the literal single quote character, one must use `''''`.
================================================
FILE: src/dateparser/docs/demo.html
================================================
================================================
FILE: src/dateparser/docs/demo.js
================================================
angular.module('ui.bootstrap.demo').controller('DateParserDemoCtrl', function ($scope, uibDateParser) {
$scope.format = 'yyyy/MM/dd';
$scope.date = new Date();
});
================================================
FILE: src/dateparser/index.js
================================================
require('./dateparser');
var MODULE_NAME = 'ui.bootstrap.module.dateparser';
angular.module(MODULE_NAME, ['ui.bootstrap.dateparser']);
module.exports = MODULE_NAME;
================================================
FILE: src/dateparser/test/dateparser.spec.js
================================================
describe('date parser', function() {
var dateParser, oldDate;
beforeEach(module('ui.bootstrap.dateparser'));
beforeEach(inject(function (uibDateParser) {
dateParser = uibDateParser;
oldDate = new Date(1, 2, 6);
oldDate.setFullYear(1);
}));
function expectFilter(date, format, display) {
expect(dateParser.filter(date, format)).toEqual(display);
}
function expectParse(input, format, date) {
expect(dateParser.parse(input, format)).toEqual(date);
}
function expectBaseParse(input, format, baseDate, date) {
expect(dateParser.parse(input, format, baseDate)).toEqual(date);
}
describe('filter', function() {
it('should work correctly for `dd`, `MM`, `yyyy`', function() {
expectFilter(new Date(2013, 10, 17, 0), 'dd.MM.yyyy', '17.11.2013');
expectFilter(new Date(2013, 11, 31, 0), 'dd.MM.yyyy', '31.12.2013');
expectFilter(new Date(1991, 2, 8, 0), 'dd-MM-yyyy', '08-03-1991');
expectFilter(new Date(1980, 2, 5, 0), 'MM/dd/yyyy', '03/05/1980');
expectFilter(new Date(1983, 0, 10, 0), 'dd.MM/yyyy', '10.01/1983');
expectFilter(new Date(1980, 10, 9, 0), 'MM-dd-yyyy', '11-09-1980');
expectFilter(new Date(2011, 1, 5, 0), 'yyyy/MM/dd', '2011/02/05');
expectFilter(oldDate, 'yyyy/MM/dd', '0001/03/06');
});
it('should work correctly for `yy`', function() {
expectFilter(new Date(2013, 10, 17, 0), 'dd.MM.yy', '17.11.13');
expectFilter(new Date(2011, 4, 2, 0), 'dd-MM-yy', '02-05-11');
expectFilter(new Date(2080, 1, 5, 0), 'MM/dd/yy', '02/05/80');
expectFilter(new Date(2055, 1, 5, 0), 'yy/MM/dd', '55/02/05');
expectFilter(new Date(2013, 7, 11, 0), 'dd-MM-yy', '11-08-13');
});
it('should work correctly for `y`', function() {
expectFilter(new Date(2013, 10, 17, 0), 'dd.MM.y', '17.11.2013');
expectFilter(new Date(2013, 11, 31, 0), 'dd.MM.y', '31.12.2013');
expectFilter(new Date(1991, 2, 8, 0), 'dd-MM-y', '08-03-1991');
expectFilter(new Date(1980, 2, 5, 0), 'MM/dd/y', '03/05/1980');
expectFilter(new Date(1983, 0, 10, 0), 'dd.MM/y', '10.01/1983');
expectFilter(new Date(1980, 10, 9, 0), 'MM-dd-y', '11-09-1980');
expectFilter(new Date(2011, 1, 5, 0), 'y/MM/dd', '2011/02/05');
});
it('should work correctly for `MMMM`', function() {
expectFilter(new Date(2013, 10, 17, 0), 'dd.MMMM.yy', '17.November.13');
expectFilter(new Date(1980, 2, 5, 0), 'dd-MMMM-yyyy', '05-March-1980');
expectFilter(new Date(1980, 1, 5, 0), 'MMMM/dd/yyyy', 'February/05/1980');
expectFilter(new Date(1949, 11, 20, 0), 'yyyy/MMMM/dd', '1949/December/20');
expectFilter(oldDate, 'yyyy/MMMM/dd', '0001/March/06');
});
it('should work correctly for `MMM`', function() {
expectFilter(new Date(2010, 8, 30, 0), 'dd.MMM.yy', '30.Sep.10');
expectFilter(new Date(2011, 4, 2, 0), 'dd-MMM-yy', '02-May-11');
expectFilter(new Date(1980, 1, 5, 0), 'MMM/dd/yyyy', 'Feb/05/1980');
expectFilter(new Date(1955, 1, 5, 0), 'yyyy/MMM/dd', '1955/Feb/05');
expectFilter(oldDate, 'yyyy/MMM/dd', '0001/Mar/06');
});
it('should work correctly for `M`', function() {
expectFilter(new Date(2013, 7, 11, 0), 'M/dd/yyyy', '8/11/2013');
expectFilter(new Date(2005, 10, 7, 0), 'dd.M.yy', '07.11.05');
expectFilter(new Date(2011, 4, 2, 0), 'dd-M-yy', '02-5-11');
expectFilter(new Date(1980, 1, 5, 0), 'M/dd/yyyy', '2/05/1980');
expectFilter(new Date(1955, 1, 5, 0), 'yyyy/M/dd', '1955/2/05');
expectFilter(new Date(2011, 4, 2, 0), 'dd-M-yy', '02-5-11');
});
it('should work correctly for `M!`', function() {
expectFilter(new Date(2013, 7, 11, 0), 'M!/dd/yyyy', '08/11/2013');
expectFilter(new Date(2005, 10, 7, 0), 'dd.M!.yy', '07.11.05');
expectFilter(new Date(2011, 4, 2, 0), 'dd-M!-yy', '02-05-11');
expectFilter(new Date(1980, 1, 5, 0), 'M!/dd/yyyy', '02/05/1980');
expectFilter(new Date(1955, 1, 5, 0), 'yyyy/M!/dd', '1955/02/05');
expectFilter(new Date(2011, 4, 2, 0), 'dd-M!-yy', '02-05-11');
expectFilter(oldDate, 'yyyy/M!/dd', '0001/03/06');
});
it('should work correctly for `LLLL`', function() {
expectFilter(new Date(2013, 7, 24, 0), 'LLLL/dd/yyyy', 'August/24/2013');
expectFilter(new Date(2004, 10, 7, 0), 'dd.LLLL.yy', '07.November.04');
expectFilter(new Date(2011, 4, 18, 0), 'dd-LLLL-yy', '18-May-11');
expectFilter(new Date(1980, 1, 5, 0), 'LLLL/dd/yyyy', 'February/05/1980');
expectFilter(new Date(1955, 2, 5, 0), 'yyyy/LLLL/dd', '1955/March/05');
expectFilter(new Date(2011, 5, 2, 0), 'dd-LLLL-yy', '02-June-11');
expectFilter(oldDate, 'yyyy/LLLL/dd', '0001/March/06');
});
it('should work correctly for `d`', function() {
expectFilter(new Date(2013, 10, 17, 0), 'd.MMMM.yy', '17.November.13');
expectFilter(new Date(1991, 2, 8, 0), 'd-MMMM-yyyy', '8-March-1991');
expectFilter(new Date(1980, 1, 5, 0), 'MMMM/d/yyyy', 'February/5/1980');
expectFilter(new Date(1955, 1, 5, 0), 'yyyy/MMMM/d', '1955/February/5');
expectFilter(new Date(2013, 7, 11, 0), 'd-MM-yy', '11-08-13');
expectFilter(oldDate, 'yyyy/MM/d', '0001/03/6');
});
it('should work correctly for `d!`', function() {
expectFilter(new Date(2013, 10, 17, 0), 'd!.MMMM.yy', '17.November.13');
expectFilter(new Date(1991, 2, 8, 0), 'd!-MMMM-yyyy', '08-March-1991');
expectFilter(new Date(1980, 1, 5, 0), 'MMMM/d!/yyyy', 'February/05/1980');
expectFilter(new Date(1955, 1, 5, 0), 'yyyy/MMMM/d!', '1955/February/05');
expectFilter(new Date(2013, 7, 11, 0), 'd!-MM-yy', '11-08-13');
expectFilter(oldDate, 'yyyy/MM/d!', '0001/03/06');
});
it('should work correctly for `EEEE`', function() {
expectFilter(new Date(2013, 10, 17, 0), 'EEEE.d.MMMM.yy', 'Sunday.17.November.13');
expectFilter(new Date(1991, 2, 8, 0), 'd-EEEE-MMMM-yyyy', '8-Friday-March-1991');
expectFilter(new Date(1980, 1, 5, 0), 'MMMM/d/yyyy/EEEE', 'February/5/1980/Tuesday');
expectFilter(new Date(1955, 1, 5, 0), 'yyyy/EEEE/MMMM/d', '1955/Saturday/February/5');
});
it('should work correctly for `EEE`', function() {
expectFilter(new Date(2013, 10, 17, 0), 'EEE.d.MMMM.yy', 'Sun.17.November.13');
expectFilter(new Date(1991, 2, 8, 0), 'd-EEE-MMMM-yyyy', '8-Fri-March-1991');
expectFilter(new Date(1980, 1, 5, 0), 'MMMM/d/yyyy/EEE', 'February/5/1980/Tue');
expectFilter(new Date(1955, 1, 5, 0), 'yyyy/EEE/MMMM/d', '1955/Sat/February/5');
});
it('should work correctly for `HH`', function() {
expectFilter(new Date(2015, 2, 22, 22), 'd.MMMM.yy.HH', '22.March.15.22');
expectFilter(new Date(1991, 2, 8, 11), 'd-MMMM-yyyy-HH', '8-March-1991-11');
expectFilter(new Date(1980, 1, 5, 0), 'MMMM/d/yyyy/HH', 'February/5/1980/00');
expectFilter(new Date(1955, 1, 5, 3), 'yyyy/MMMM/d HH', '1955/February/5 03');
expectFilter(new Date(2013, 7, 11, 23), 'd-MM-yy HH', '11-08-13 23');
});
it('should work correctly for `H`', function() {
expectFilter(new Date(2015, 2, 22, 22), 'd.MMMM.yy.H', '22.March.15.22');
expectFilter(new Date(1991, 2, 8, 11), 'd-MMMM-yyyy-H', '8-March-1991-11');
expectFilter(new Date(1980, 1, 5, 0), 'MMMM/d/yyyy/H', 'February/5/1980/0');
expectFilter(new Date(1955, 1, 5, 3), 'yyyy/MMMM/d H', '1955/February/5 3');
expectFilter(new Date(2013, 7, 11, 23), 'd-MM-yy H', '11-08-13 23');
});
it('should work correctly for `hh`', function() {
expectFilter(new Date(2015, 2, 22, 12), 'd.MMMM.yy.hh', '22.March.15.12');
expectFilter(new Date(1991, 2, 8, 11), 'd-MMMM-yyyy-hh', '8-March-1991-11');
expectFilter(new Date(1980, 1, 5, 0), 'MMMM/d/yyyy/hh', 'February/5/1980/12');
expectFilter(new Date(1955, 1, 5, 3), 'yyyy/MMMM/d hh', '1955/February/5 03');
expectFilter(new Date(2013, 7, 11, 9), 'd-MM-yy hh', '11-08-13 09');
});
it('should work correctly for `h`', function() {
expectFilter(new Date(2015, 2, 22, 12), 'd.MMMM.yy.h', '22.March.15.12');
expectFilter(new Date(1991, 2, 8, 11), 'd-MMMM-yyyy-h', '8-March-1991-11');
expectFilter(new Date(1980, 1, 5, 0), 'MMMM/d/yyyy/h', 'February/5/1980/12');
expectFilter(new Date(1955, 1, 5, 3), 'yyyy/MMMM/d h', '1955/February/5 3');
expectFilter(new Date(2013, 7, 11, 3), 'd-MM-yy h', '11-08-13 3');
});
it('should work correctly for `mm`', function() {
expectFilter(new Date(2015, 2, 22, 0, 22), 'd.MMMM.yy.mm', '22.March.15.22');
expectFilter(new Date(1991, 2, 8, 0, 59), 'd-MMMM-yyyy-mm', '8-March-1991-59');
expectFilter(new Date(1980, 1, 5, 0, 0), 'MMMM/d/yyyy/mm', 'February/5/1980/00');
expectFilter(new Date(1955, 1, 5, 0, 3), 'yyyy/MMMM/d mm', '1955/February/5 03');
expectFilter(new Date(2013, 7, 11, 0, 46), 'd-MM-yy mm', '11-08-13 46');
expectFilter(new Date(2015, 2, 22, 22, 33), 'd.MMMM.yy.HH:mm', '22.March.15.22:33');
expectFilter(new Date(2015, 2, 22, 2, 1), 'd.MMMM.yy.H:mm', '22.March.15.2:01');
});
it('should work correctly for `m`', function() {
expectFilter(new Date(2015, 2, 22, 0, 22), 'd.MMMM.yy.m', '22.March.15.22');
expectFilter(new Date(1991, 2, 8, 0, 59), 'd-MMMM-yyyy-m', '8-March-1991-59');
expectFilter(new Date(1980, 1, 5, 0, 0), 'MMMM/d/yyyy/m', 'February/5/1980/0');
expectFilter(new Date(1955, 1, 5, 0, 3), 'yyyy/MMMM/d m', '1955/February/5 3');
expectFilter(new Date(2013, 7, 11, 0, 46), 'd-MM-yy m', '11-08-13 46');
expectFilter(new Date(2015, 2, 22, 22, 3), 'd.MMMM.yy.HH:m', '22.March.15.22:3');
expectFilter(new Date(2015, 2, 22, 2, 1), 'd.MMMM.yy.H:m', '22.March.15.2:1');
});
it('should work correctly for `sss`', function() {
expectFilter(new Date(2015, 2, 22, 0, 0, 0, 123), 'd.MMMM.yy.sss', '22.March.15.123');
expectFilter(new Date(1991, 2, 8, 0, 0, 0, 59), 'd-MMMM-yyyy-sss', '8-March-1991-059');
expectFilter(new Date(1980, 1, 5, 0, 0, 0), 'MMMM/d/yyyy/sss', 'February/5/1980/000');
expectFilter(new Date(1955, 1, 5, 0, 0, 0, 3), 'yyyy/MMMM/d sss', '1955/February/5 003');
expectFilter(new Date(2013, 7, 11, 0, 0, 0, 46), 'd-MM-yy sss', '11-08-13 046');
expectFilter(new Date(2015, 2, 22, 22, 33, 0, 44), 'd.MMMM.yy.HH:mm:sss', '22.March.15.22:33:044');
expectFilter(new Date(2015, 2, 22, 0, 0, 0, 1), 'd.MMMM.yy.H:m:sss', '22.March.15.0:0:001');
});
it('should work correctly for `ss`', function() {
expectFilter(new Date(2015, 2, 22, 0, 0, 22), 'd.MMMM.yy.ss', '22.March.15.22');
expectFilter(new Date(1991, 2, 8, 0, 0, 59), 'd-MMMM-yyyy-ss', '8-March-1991-59');
expectFilter(new Date(1980, 1, 5, 0, 0, 0), 'MMMM/d/yyyy/ss', 'February/5/1980/00');
expectFilter(new Date(1955, 1, 5, 0, 0, 3), 'yyyy/MMMM/d ss', '1955/February/5 03');
expectFilter(new Date(2013, 7, 11, 0, 0, 46), 'd-MM-yy ss', '11-08-13 46');
expectFilter(new Date(2015, 2, 22, 22, 33, 44), 'd.MMMM.yy.HH:mm:ss', '22.March.15.22:33:44');
expectFilter(new Date(2015, 2, 22, 0, 0, 1), 'd.MMMM.yy.H:m:ss', '22.March.15.0:0:01');
});
it('should work correctly for `s`', function() {
expectFilter(new Date(2015, 2, 22, 0, 0, 22), 'd.MMMM.yy.s', '22.March.15.22');
expectFilter(new Date(1991, 2, 8, 0, 0, 59), 'd-MMMM-yyyy-s', '8-March-1991-59');
expectFilter(new Date(1980, 1, 5, 0, 0, 0), 'MMMM/d/yyyy/s', 'February/5/1980/0');
expectFilter(new Date(1955, 1, 5, 0, 0, 3), 'yyyy/MMMM/d s', '1955/February/5 3');
expectFilter(new Date(2013, 7, 11, 0, 0, 46), 'd-MM-yy s', '11-08-13 46');
expectFilter(new Date(2015, 2, 22, 22, 33, 4), 'd.MMMM.yy.HH:mm:s', '22.March.15.22:33:4');
expectFilter(new Date(2015, 2, 22, 22, 3, 4), 'd.MMMM.yy.HH:m:s', '22.March.15.22:3:4');
});
it('should work correctly for `a`', function() {
expectFilter(new Date(2015, 2, 22, 10), 'd.MMMM.yy.hha', '22.March.15.10AM');
expectFilter(new Date(2015, 2, 22, 22), 'd.MMMM.yy.hha', '22.March.15.10PM');
expectFilter(new Date(1991, 2, 8, 11), 'd-MMMM-yyyy-hha', '8-March-1991-11AM');
expectFilter(new Date(1991, 2, 8, 23), 'd-MMMM-yyyy-hha', '8-March-1991-11PM');
expectFilter(new Date(1980, 1, 5, 0), 'MMMM/d/yyyy/hha', 'February/5/1980/12AM');
expectFilter(new Date(1980, 1, 5, 12), 'MMMM/d/yyyy/hha', 'February/5/1980/12PM');
expectFilter(new Date(1955, 1, 5, 3), 'yyyy/MMMM/d hha', '1955/February/5 03AM');
expectFilter(new Date(1955, 1, 5, 15), 'yyyy/MMMM/d hha', '1955/February/5 03PM');
expectFilter(new Date(2013, 7, 11, 9), 'd-MM-yy hha', '11-08-13 09AM');
expectFilter(new Date(2013, 7, 11, 21), 'd-MM-yy hha', '11-08-13 09PM');
});
it('should work correctly for `ww`', function() {
expectFilter(new Date(2013, 10, 17, 0), 'd.MMMM.yy.ww', '17.November.13.47');
expectFilter(new Date(1991, 2, 8, 0), 'd-MMMM-yyyy-ww', '8-March-1991-10');
expectFilter(new Date(1980, 1, 5, 0), 'MMMM/d/yyyy/ww', 'February/5/1980/06');
expectFilter(new Date(1955, 1, 5, 0), 'yyyy/MMMM/d/ww', '1955/February/5/05');
expectFilter(new Date(2013, 7, 11, 0), 'd-MM-yy ww', '11-08-13 33');
expectFilter(oldDate, 'yyyy/MM/d ww', '0001/03/6 10');
});
it('should work correctly for `w`', function() {
expectFilter(new Date(2013, 10, 17, 0), 'd.MMMM.yy.w', '17.November.13.47');
expectFilter(new Date(1991, 2, 8, 0), 'd-MMMM-yyyy-w', '8-March-1991-10');
expectFilter(new Date(1980, 1, 5, 0), 'MMMM/d/yyyy/w', 'February/5/1980/6');
expectFilter(new Date(1955, 1, 5, 0), 'yyyy/MMMM/d/w', '1955/February/5/5');
expectFilter(new Date(2013, 7, 11, 0), 'd-MM-yy w', '11-08-13 33');
expectFilter(oldDate, 'yyyy/MM/d w', '0001/03/6 10');
});
it('should work correctly for `G`', function() {
expectFilter(new Date(2013, 10, 17, 0), 'd.MMMM.yy.G', '17.November.13.AD');
expectFilter(new Date(-1991, 2, 8, 0), 'd-MMMM-yyyy-G', '8-March-1991-BC');
expectFilter(new Date(1980, 1, 5, 0), 'MMMM/d/yyyy/G', 'February/5/1980/AD');
expectFilter(new Date(-1955, 1, 5, 0), 'yyyy/MMMM/d/G', '1955/February/5/BC');
expectFilter(new Date(2013, 7, 11, 0), 'd-MM-yy G', '11-08-13 AD');
});
it('should work correctly for `GG`', function() {
expectFilter(new Date(2013, 10, 17, 0), 'd.MMMM.yy.GG', '17.November.13.AD');
expectFilter(new Date(-1991, 2, 8, 0), 'd-MMMM-yyyy-GG', '8-March-1991-BC');
expectFilter(new Date(1980, 1, 5, 0), 'MMMM/d/yyyy/GG', 'February/5/1980/AD');
expectFilter(new Date(-1955, 1, 5, 0), 'yyyy/MMMM/d/GG', '1955/February/5/BC');
expectFilter(new Date(2013, 7, 11, 0), 'd-MM-yy GG', '11-08-13 AD');
});
it('should work correctly for `GGG`', function() {
expectFilter(new Date(2013, 10, 17, 0), 'd.MMMM.yy.GGG', '17.November.13.AD');
expectFilter(new Date(-1991, 2, 8, 0), 'd-MMMM-yyyy-GGG', '8-March-1991-BC');
expectFilter(new Date(1980, 1, 5, 0), 'MMMM/d/yyyy/GGG', 'February/5/1980/AD');
expectFilter(new Date(-1955, 1, 5, 0), 'yyyy/MMMM/d/GGG', '1955/February/5/BC');
expectFilter(new Date(2013, 7, 11, 0), 'd-MM-yy GGG', '11-08-13 AD');
});
it('should work correctly for `GGGG`', function() {
expectFilter(new Date(2013, 10, 17, 0), 'd.MMMM.yy.GGGG', '17.November.13.Anno Domini');
expectFilter(new Date(-1991, 2, 8, 0), 'd-MMMM-yyyy-GGGG', '8-March-1991-Before Christ');
expectFilter(new Date(1980, 1, 5, 0), 'MMMM/d/yyyy/GGGG', 'February/5/1980/Anno Domini');
expectFilter(new Date(-1955, 1, 5, 0), 'yyyy/MMMM/d/GGGG', '1955/February/5/Before Christ');
expectFilter(new Date(2013, 7, 11, 0), 'd-MM-yy GGGG', '11-08-13 Anno Domini');
});
it('should work correctly for literal text', function() {
expectFilter(new Date(2013, 10, 17, 0), 'dd.MM.yyyy foo', '17.11.2013 foo');
});
});
describe('with custom formats', function() {
it('should work correctly for `dd`, `MM`, `yyyy`', function() {
expectParse('17.11.2013', 'dd.MM.yyyy', new Date(2013, 10, 17, 0));
expectParse('31.12.2013', 'dd.MM.yyyy', new Date(2013, 11, 31, 0));
expectParse('08-03-1991', 'dd-MM-yyyy', new Date(1991, 2, 8, 0));
expectParse('03/05/1980', 'MM/dd/yyyy', new Date(1980, 2, 5, 0));
expectParse('10.01/1983', 'dd.MM/yyyy', new Date(1983, 0, 10, 0));
expectParse('11-09-1980', 'MM-dd-yyyy', new Date(1980, 10, 9, 0));
expectParse('2011/02/05', 'yyyy/MM/dd', new Date(2011, 1, 5, 0));
expectParse('0001/03/06', 'yyyy/MM/dd', oldDate);
});
it('should work correctly for `yy`', function() {
expectParse('17.11.13', 'dd.MM.yy', new Date(2013, 10, 17, 0));
expectParse('02-05-11', 'dd-MM-yy', new Date(2011, 4, 2, 0));
expectParse('02/05/80', 'MM/dd/yy', new Date(1980, 1, 5, 0));
expectParse('55/02/05', 'yy/MM/dd', new Date(2055, 1, 5, 0));
expectParse('11-08-13', 'dd-MM-yy', new Date(2013, 7, 11, 0));
});
it('should use `68` as the pivot year for `yy`', function() {
expectParse('17.11.68', 'dd.MM.yy', new Date(2068, 10, 17, 0));
expectParse('17.11.69', 'dd.MM.yy', new Date(1969, 10, 17, 0));
});
it('should work correctly for `y`', function() {
expectParse('17.11.2013', 'dd.MM.y', new Date(2013, 10, 17, 0));
expectParse('31.12.2013', 'dd.MM.y', new Date(2013, 11, 31, 0));
expectParse('08-03-1991', 'dd-MM-y', new Date(1991, 2, 8, 0));
expectParse('03/05/1980', 'MM/dd/y', new Date(1980, 2, 5, 0));
expectParse('10.01/1983', 'dd.MM/y', new Date(1983, 0, 10, 0));
expectParse('11-09-1980', 'MM-dd-y', new Date(1980, 10, 9, 0));
expectParse('2011/02/05', 'y/MM/dd', new Date(2011, 1, 5, 0));
});
it('should work correctly for `MMMM`', function() {
expectParse('17.November.13', 'dd.MMMM.yy', new Date(2013, 10, 17, 0));
expectParse('05-March-1980', 'dd-MMMM-yyyy', new Date(1980, 2, 5, 0));
expectParse('February/05/1980', 'MMMM/dd/yyyy', new Date(1980, 1, 5, 0));
expectParse('1949/December/20', 'yyyy/MMMM/dd', new Date(1949, 11, 20, 0));
expectParse('0001/March/06', 'yyyy/MMMM/dd', oldDate);
});
it('should work correctly for `MMM`', function() {
expectParse('30.Sep.10', 'dd.MMM.yy', new Date(2010, 8, 30, 0));
expectParse('02-May-11', 'dd-MMM-yy', new Date(2011, 4, 2, 0));
expectParse('Feb/05/1980', 'MMM/dd/yyyy', new Date(1980, 1, 5, 0));
expectParse('1955/Feb/05', 'yyyy/MMM/dd', new Date(1955, 1, 5, 0));
expectParse('0001/Mar/06', 'yyyy/MMM/dd', oldDate);
});
it('should work correctly for `M`', function() {
expectParse('8/11/2013', 'M/dd/yyyy', new Date(2013, 7, 11, 0));
expectParse('07.11.05', 'dd.M.yy', new Date(2005, 10, 7, 0));
expectParse('02-5-11', 'dd-M-yy', new Date(2011, 4, 2, 0));
expectParse('2/05/1980', 'M/dd/yyyy', new Date(1980, 1, 5, 0));
expectParse('1955/2/05', 'yyyy/M/dd', new Date(1955, 1, 5, 0));
expectParse('02-5-11', 'dd-M-yy', new Date(2011, 4, 2, 0));
});
it('should work correctly for `M!`', function() {
expectParse('8/11/2013', 'M!/dd/yyyy', new Date(2013, 7, 11, 0));
expectParse('07.11.05', 'dd.M!.yy', new Date(2005, 10, 7, 0));
expectParse('02-5-11', 'dd-M!-yy', new Date(2011, 4, 2, 0));
expectParse('2/05/1980', 'M!/dd/yyyy', new Date(1980, 1, 5, 0));
expectParse('1955/2/05', 'yyyy/M!/dd', new Date(1955, 1, 5, 0));
expectParse('02-5-11', 'dd-M!-yy', new Date(2011, 4, 2, 0));
expectParse('0001/3/06', 'yyyy/M!/dd', oldDate);
expectParse('08/11/2013', 'M!/dd/yyyy', new Date(2013, 7, 11, 0));
expectParse('07.11.05', 'dd.M!.yy', new Date(2005, 10, 7, 0));
expectParse('02-05-11', 'dd-M!-yy', new Date(2011, 4, 2, 0));
expectParse('02/05/1980', 'M!/dd/yyyy', new Date(1980, 1, 5, 0));
expectParse('1955/02/05', 'yyyy/M!/dd', new Date(1955, 1, 5, 0));
expectParse('02-05-11', 'dd-M!-yy', new Date(2011, 4, 2, 0));
expectParse('0001/03/06', 'yyyy/M!/dd', oldDate);
});
it('should work correctly for `d`', function() {
expectParse('17.November.13', 'd.MMMM.yy', new Date(2013, 10, 17, 0));
expectParse('8-March-1991', 'd-MMMM-yyyy', new Date(1991, 2, 8, 0));
expectParse('February/5/1980', 'MMMM/d/yyyy', new Date(1980, 1, 5, 0));
expectParse('1955/February/5', 'yyyy/MMMM/d', new Date(1955, 1, 5, 0));
expectParse('11-08-13', 'd-MM-yy', new Date(2013, 7, 11, 0));
expectParse('0001/03/6', 'yyyy/MM/d', oldDate);
});
it('should work correctly for `d!`', function() {
expectParse('17.November.13', 'd!.MMMM.yy', new Date(2013, 10, 17, 0));
expectParse('8-March-1991', 'd!-MMMM-yyyy', new Date(1991, 2, 8, 0));
expectParse('February/5/1980', 'MMMM/d!/yyyy', new Date(1980, 1, 5, 0));
expectParse('1955/February/5', 'yyyy/MMMM/d!', new Date(1955, 1, 5, 0));
expectParse('11-08-13', 'd!-MM-yy', new Date(2013, 7, 11, 0));
expectParse('0001/03/6', 'yyyy/MM/d!', oldDate);
expectParse('17.November.13', 'd!.MMMM.yy', new Date(2013, 10, 17, 0));
expectParse('08-March-1991', 'd!-MMMM-yyyy', new Date(1991, 2, 8, 0));
expectParse('February/05/1980', 'MMMM/d!/yyyy', new Date(1980, 1, 5, 0));
expectParse('1955/February/05', 'yyyy/MMMM/d!', new Date(1955, 1, 5, 0));
expectParse('11-08-13', 'd!-MM-yy', new Date(2013, 7, 11, 0));
expectParse('0001/03/06', 'yyyy/MM/d!', oldDate);
});
it('should work correctly for `EEEE`', function() {
expectParse('Sunday.17.November.13', 'EEEE.d.MMMM.yy', new Date(2013, 10, 17, 0));
expectParse('8-Friday-March-1991', 'd-EEEE-MMMM-yyyy', new Date(1991, 2, 8, 0));
expectParse('February/5/1980/Tuesday', 'MMMM/d/yyyy/EEEE', new Date(1980, 1, 5, 0));
expectParse('1955/Saturday/February/5', 'yyyy/EEEE/MMMM/d', new Date(1955, 1, 5, 0));
});
it('should work correctly for `EEE`', function() {
expectParse('Sun.17.November.13', 'EEE.d.MMMM.yy', new Date(2013, 10, 17, 0));
expectParse('8-Fri-March-1991', 'd-EEE-MMMM-yyyy', new Date(1991, 2, 8, 0));
expectParse('February/5/1980/Tue', 'MMMM/d/yyyy/EEE', new Date(1980, 1, 5, 0));
expectParse('1955/Sat/February/5', 'yyyy/EEE/MMMM/d', new Date(1955, 1, 5, 0));
});
it('should work correctly for `HH`', function() {
expectParse('22.March.15.22', 'd.MMMM.yy.HH', new Date(2015, 2, 22, 22));
expectParse('8-March-1991-11', 'd-MMMM-yyyy-HH', new Date(1991, 2, 8, 11));
expectParse('February/5/1980/00', 'MMMM/d/yyyy/HH', new Date(1980, 1, 5, 0));
expectParse('1955/February/5 03', 'yyyy/MMMM/d HH', new Date(1955, 1, 5, 3));
expectParse('11-08-13 23', 'd-MM-yy HH', new Date(2013, 7, 11, 23));
});
it('should work correctly for `H`', function() {
expectParse('22.March.15.22', 'd.MMMM.yy.H', new Date(2015, 2, 22, 22));
expectParse('8-March-1991-11', 'd-MMMM-yyyy-H', new Date(1991, 2, 8, 11));
expectParse('February/5/1980/0', 'MMMM/d/yyyy/H', new Date(1980, 1, 5, 0));
expectParse('1955/February/5 3', 'yyyy/MMMM/d H', new Date(1955, 1, 5, 3));
expectParse('11-08-13 23', 'd-MM-yy H', new Date(2013, 7, 11, 23));
});
it('should work correctly for `hh`', function() {
expectParse('22.March.15.22', 'd.MMMM.yy.hh', undefined);
expectParse('22.March.15.12', 'd.MMMM.yy.hh', new Date(2015, 2, 22, 12));
expectParse('8-March-1991-11', 'd-MMMM-yyyy-hh', new Date(1991, 2, 8, 11));
expectParse('February/5/1980/00', 'MMMM/d/yyyy/hh', new Date(1980, 1, 5, 0));
expectParse('1955/February/5 03', 'yyyy/MMMM/d hh', new Date(1955, 1, 5, 3));
expectParse('11-08-13 23', 'd-MM-yy hh', undefined);
expectParse('11-08-13 09', 'd-MM-yy hh', new Date(2013, 7, 11, 9));
});
it('should work correctly for `h`', function() {
expectParse('22.March.15.12', 'd.MMMM.yy.h', new Date(2015, 2, 22, 12));
expectParse('8-March-1991-11', 'd-MMMM-yyyy-h', new Date(1991, 2, 8, 11));
expectParse('February/5/1980/0', 'MMMM/d/yyyy/h', new Date(1980, 1, 5, 0));
expectParse('1955/February/5 3', 'yyyy/MMMM/d h', new Date(1955, 1, 5, 3));
expectParse('11-08-13 3', 'd-MM-yy h', new Date(2013, 7, 11, 3));
});
it('should work correctly for `mm`', function() {
expectParse('22.March.15.22', 'd.MMMM.yy.mm', new Date(2015, 2, 22, 0, 22));
expectParse('8-March-1991-59', 'd-MMMM-yyyy-mm', new Date(1991, 2, 8, 0, 59));
expectParse('February/5/1980/00', 'MMMM/d/yyyy/mm', new Date(1980, 1, 5, 0, 0));
expectParse('1955/February/5 03', 'yyyy/MMMM/d mm', new Date(1955, 1, 5, 0, 3));
expectParse('11-08-13 46', 'd-MM-yy mm', new Date(2013, 7, 11, 0, 46));
expectParse('22.March.15.22:33', 'd.MMMM.yy.HH:mm', new Date(2015, 2, 22, 22, 33));
expectParse('22.March.15.2:01', 'd.MMMM.yy.H:mm', new Date(2015, 2, 22, 2, 1));
});
it('should work correctly for `m`', function() {
expectParse('22.March.15.22', 'd.MMMM.yy.m', new Date(2015, 2, 22, 0, 22));
expectParse('8-March-1991-59', 'd-MMMM-yyyy-m', new Date(1991, 2, 8, 0, 59));
expectParse('February/5/1980/0', 'MMMM/d/yyyy/m', new Date(1980, 1, 5, 0, 0));
expectParse('1955/February/5 3', 'yyyy/MMMM/d m', new Date(1955, 1, 5, 0, 3));
expectParse('11-08-13 46', 'd-MM-yy m', new Date(2013, 7, 11, 0, 46));
expectParse('22.March.15.22:3', 'd.MMMM.yy.HH:m', new Date(2015, 2, 22, 22, 3));
expectParse('22.March.15.2:1', 'd.MMMM.yy.H:m', new Date(2015, 2, 22, 2, 1));
});
it('should work correctly for `sss`', function() {
expectParse('22.March.15.123', 'd.MMMM.yy.sss', new Date(2015, 2, 22, 0, 0, 0, 123));
expectParse('8-March-1991-059', 'd-MMMM-yyyy-sss', new Date(1991, 2, 8, 0, 0, 0, 59));
expectParse('February/5/1980/000', 'MMMM/d/yyyy/sss', new Date(1980, 1, 5, 0, 0, 0));
expectParse('1955/February/5 003', 'yyyy/MMMM/d sss', new Date(1955, 1, 5, 0, 0, 0, 3));
expectParse('11-08-13 046', 'd-MM-yy sss', new Date(2013, 7, 11, 0, 0, 0, 46));
expectParse('22.March.15.22:33:044', 'd.MMMM.yy.HH:mm:sss', new Date(2015, 2, 22, 22, 33, 0, 44));
expectParse('22.March.15.0:0:001', 'd.MMMM.yy.H:m:sss', new Date(2015, 2, 22, 0, 0, 0, 1));
});
it('should work correctly for `ss`', function() {
expectParse('22.March.15.22', 'd.MMMM.yy.ss', new Date(2015, 2, 22, 0, 0, 22));
expectParse('8-March-1991-59', 'd-MMMM-yyyy-ss', new Date(1991, 2, 8, 0, 0, 59));
expectParse('February/5/1980/00', 'MMMM/d/yyyy/ss', new Date(1980, 1, 5, 0, 0, 0));
expectParse('1955/February/5 03', 'yyyy/MMMM/d ss', new Date(1955, 1, 5, 0, 0, 3));
expectParse('11-08-13 46', 'd-MM-yy ss', new Date(2013, 7, 11, 0, 0, 46));
expectParse('22.March.15.22:33:44', 'd.MMMM.yy.HH:mm:ss', new Date(2015, 2, 22, 22, 33, 44));
expectParse('22.March.15.0:0:01', 'd.MMMM.yy.H:m:ss', new Date(2015, 2, 22, 0, 0, 1));
});
it('should work correctly for `s`', function() {
expectParse('22.March.15.22', 'd.MMMM.yy.s', new Date(2015, 2, 22, 0, 0, 22));
expectParse('8-March-1991-59', 'd-MMMM-yyyy-s', new Date(1991, 2, 8, 0, 0, 59));
expectParse('February/5/1980/0', 'MMMM/d/yyyy/s', new Date(1980, 1, 5, 0, 0, 0));
expectParse('1955/February/5 3', 'yyyy/MMMM/d s', new Date(1955, 1, 5, 0, 0, 3));
expectParse('11-08-13 46', 'd-MM-yy s', new Date(2013, 7, 11, 0, 0, 46));
expectParse('22.March.15.22:33:4', 'd.MMMM.yy.HH:mm:s', new Date(2015, 2, 22, 22, 33, 4));
expectParse('22.March.15.22:3:4', 'd.MMMM.yy.HH:m:s', new Date(2015, 2, 22, 22, 3, 4));
});
it('should work correctly for `a`', function() {
expectParse('22.March.15.10AM', 'd.MMMM.yy.hha', new Date(2015, 2, 22, 10));
expectParse('22.March.15.10PM', 'd.MMMM.yy.hha', new Date(2015, 2, 22, 22));
expectParse('8-March-1991-11AM', 'd-MMMM-yyyy-hha', new Date(1991, 2, 8, 11));
expectParse('8-March-1991-11PM', 'd-MMMM-yyyy-hha', new Date(1991, 2, 8, 23));
expectParse('February/5/1980/12AM', 'MMMM/d/yyyy/hha', new Date(1980, 1, 5, 0));
expectParse('February/5/1980/12PM', 'MMMM/d/yyyy/hha', new Date(1980, 1, 5, 12));
expectParse('1955/February/5 03AM', 'yyyy/MMMM/d hha', new Date(1955, 1, 5, 3));
expectParse('1955/February/5 03PM', 'yyyy/MMMM/d hha', new Date(1955, 1, 5, 15));
expectParse('11-08-13 09AM', 'd-MM-yy hha', new Date(2013, 7, 11, 9));
expectParse('11-08-13 09PM', 'd-MM-yy hha', new Date(2013, 7, 11, 21));
});
it('should work correctly for `Z`', function() {
expectParse('22.March.15 -0700', 'd.MMMM.yy Z', new Date(2015, 2, 21, 17, 0, 0));
expectParse('8-March-1991 +0800', 'd-MMMM-yyyy Z', new Date(1991, 2, 8, 8, 0, 0));
expectParse('February/5/1980 -0200', 'MMMM/d/yyyy Z', new Date(1980, 1, 4, 22, 0, 0));
expectParse('1955/February/5 +0400', 'yyyy/MMMM/d Z', new Date(1955, 1, 5, 4, 0, 0));
expectParse('11-08-13 -1234', 'd-MM-yy Z', new Date(2013, 7, 10, 11, 26, 0));
expectParse('22.March.15.22:33:4 -1200', 'd.MMMM.yy.HH:mm:s Z', new Date(2015, 2, 22, 10, 33, 4));
expectParse('22.March.15.22:3:4 +1500', 'd.MMMM.yy.HH:m:s Z', new Date(2015, 2, 23, 13, 3, 4));
});
it('should work correctly for `ww`', function() {
expectParse('17.November.13.45', 'd.MMMM.yy.ww', new Date(2013, 10, 17, 0));
expectParse('8-March-1991-09', 'd-MMMM-yyyy-ww', new Date(1991, 2, 8, 0));
expectParse('February/5/1980/05', 'MMMM/d/yyyy/ww', new Date(1980, 1, 5, 0));
expectParse('1955/February/5/04', 'yyyy/MMMM/d/ww', new Date(1955, 1, 5, 0));
expectParse('11-08-13 44', 'd-MM-yy ww', new Date(2013, 7, 11, 0));
expectParse('0001/03/6 10', 'yyyy/MM/d ww', oldDate);
});
it('should work correctly for `w`', function() {
expectParse('17.November.13.45', 'd.MMMM.yy.w', new Date(2013, 10, 17, 0));
expectParse('8-March-1991-9', 'd-MMMM-yyyy-w', new Date(1991, 2, 8, 0));
expectParse('February/5/1980/5', 'MMMM/d/yyyy/w', new Date(1980, 1, 5, 0));
expectParse('1955/February/5/4', 'yyyy/MMMM/d/w', new Date(1955, 1, 5, 0));
expectParse('11-08-13 44', 'd-MM-yy w', new Date(2013, 7, 11, 0));
expectParse('0001/03/6 10', 'yyyy/MM/d w', oldDate);
});
it('should work correctly for `G`', function() {
expectParse('17.November.13.AD', 'd.MMMM.yy.G', new Date(2013, 10, 17, 0));
expectParse('8-March-1991-BC', 'd-MMMM-yyyy-G', new Date(1991, 2, 8, 0));
expectParse('February/5/1980/AD', 'MMMM/d/yyyy/G', new Date(1980, 1, 5, 0));
expectParse('1955/February/5/BC', 'yyyy/MMMM/d/G', new Date(1955, 1, 5, 0));
expectParse('11-08-13 AD', 'd-MM-yy G', new Date(2013, 7, 11, 0));
expectParse('0001/03/6 BC', 'yyyy/MM/d G', oldDate);
});
it('should work correctly for `GG`', function() {
expectParse('17.November.13.AD', 'd.MMMM.yy.GG', new Date(2013, 10, 17, 0));
expectParse('8-March-1991-BC', 'd-MMMM-yyyy-GG', new Date(1991, 2, 8, 0));
expectParse('February/5/1980/AD', 'MMMM/d/yyyy/GG', new Date(1980, 1, 5, 0));
expectParse('1955/February/5/BC', 'yyyy/MMMM/d/GG', new Date(1955, 1, 5, 0));
expectParse('11-08-13 AD', 'd-MM-yy GG', new Date(2013, 7, 11, 0));
expectParse('0001/03/6 BC', 'yyyy/MM/d GG', oldDate);
});
it('should work correctly for `GGG`', function() {
expectParse('17.November.13.AD', 'd.MMMM.yy.GGG', new Date(2013, 10, 17, 0));
expectParse('8-March-1991-BC', 'd-MMMM-yyyy-GGG', new Date(1991, 2, 8, 0));
expectParse('February/5/1980/AD', 'MMMM/d/yyyy/GGG', new Date(1980, 1, 5, 0));
expectParse('1955/February/5/BC', 'yyyy/MMMM/d/GGG', new Date(1955, 1, 5, 0));
expectParse('11-08-13 AD', 'd-MM-yy GGG', new Date(2013, 7, 11, 0));
expectParse('0001/03/6 BC', 'yyyy/MM/d GGG', oldDate);
});
it('should work correctly for `GGGG`', function() {
expectParse('17.November.13.Anno Domini', 'd.MMMM.yy.GGGG', new Date(2013, 10, 17, 0));
expectParse('8-March-1991-Before Christ', 'd-MMMM-yyyy-GGGG', new Date(1991, 2, 8, 0));
expectParse('February/5/1980/Anno Domini', 'MMMM/d/yyyy/GGGG', new Date(1980, 1, 5, 0));
expectParse('1955/February/5/Before Christ', 'yyyy/MMMM/d/GGGG', new Date(1955, 1, 5, 0));
expectParse('11-08-13 Anno Domini', 'd-MM-yy GGGG', new Date(2013, 7, 11, 0));
expectParse('0001/03/6 Before Christ', 'yyyy/MM/d GGGG', oldDate);
});
});
describe('with predefined formats', function() {
it('should work correctly for `shortDate`', function() {
expectParse('9/3/10', 'shortDate', new Date(2010, 8, 3, 0));
});
it('should work correctly for `mediumDate`', function() {
expectParse('Sep 3, 2010', 'mediumDate', new Date(2010, 8, 3, 0));
});
it('should work correctly for `longDate`', function() {
expectParse('September 3, 2010', 'longDate', new Date(2010, 8, 3, 0));
});
it('should work correctly for `fullDate`', function() {
expectParse('Friday, September 3, 2010', 'fullDate', new Date(2010, 8, 3, 0));
});
});
describe('with value literals', function() {
describe('filter', function() {
it('should work with multiple literals', function() {
expect(dateParser.filter(new Date(2013, 0, 29), 'd \'de\' MMMM \'de\' y')).toEqual('29 de January de 2013');
});
it('should work with escaped single quote', function() {
expect(dateParser.filter(new Date(2015, 2, 22, 12), 'd.MMMM.yy h \'o\'\'clock\'')).toEqual('22.March.15 12 o\'clock');
});
it('should work with only a single quote', function() {
expect(dateParser.filter(new Date(2015, 2, 22), 'd.MMMM.yy \'\'\'')).toEqual('22.March.15 \'');
});
it('should work with trailing literal', function() {
expect(dateParser.filter(new Date(2013, 0, 1), '\'year\' y')).toEqual('year 2013');
});
it('should work without whitespace', function() {
expect(dateParser.filter(new Date(2013, 0, 1), '\'year:\'y')).toEqual('year:2013');
});
});
describe('parse', function() {
it('should work with multiple literals', function() {
expect(dateParser.parse('29 de January de 2013', 'd \'de\' MMMM \'de\' y')).toEqual(new Date(2013, 0, 29));
});
it('should work with escaped single quote', function() {
expect(dateParser.parse('22.March.15 12 o\'clock', 'd.MMMM.yy h \'o\'\'clock\'')).toEqual(new Date(2015, 2, 22, 12));
});
it('should work with only a single quote', function() {
expect(dateParser.parse('22.March.15 \'', 'd.MMMM.yy \'\'\'')).toEqual(new Date(2015, 2, 22));
});
it('should work with trailing literal', function() {
expect(dateParser.parse('year 2013', '\'year\' y')).toEqual(new Date(2013, 0, 1));
});
it('should work without whitespace', function() {
expect(dateParser.parse('year:2013', '\'year:\'y')).toEqual(new Date(2013, 0, 1));
});
});
});
describe('with edge case', function() {
it('should not work for invalid number of days in February', function() {
expectParse('29.02.2013', 'dd.MM.yyyy', undefined);
});
it('should not work for 0 number of days', function() {
expectParse('00.02.2013', 'dd.MM.yyyy', undefined);
});
it('should work for 29 days in February for leap years', function() {
expectParse('29.02.2000', 'dd.MM.yyyy', new Date(2000, 1, 29, 0));
});
it('should not work for 31 days for some months', function() {
expectParse('31-04-2013', 'dd-MM-yyyy', undefined);
expectParse('November 31, 2013', 'MMMM d, yyyy', undefined);
});
});
describe('base date', function() {
var baseDate;
beforeEach(function() {
baseDate = new Date(2010, 10, 10);
});
it('should pre-initialize our date with a base date', function() {
expect(expectBaseParse('2015', 'yyyy', baseDate, new Date(2015, 10, 10)));
expect(expectBaseParse('1', 'M', baseDate, new Date(2010, 0, 10)));
expect(expectBaseParse('1', 'd', baseDate, new Date(2010, 10, 1)));
});
it('should ignore the base date when it is an invalid date', inject(function($log) {
spyOn($log, 'warn');
expect(expectBaseParse('30-12', 'dd-MM', new Date('foo'), new Date(1900, 11, 30)));
expect(expectBaseParse('30-2015', 'dd-yyyy', 'I am a cat', new Date(2015, 0, 30)));
expect($log.warn).toHaveBeenCalledWith('dateparser:', 'baseDate is not a valid date');
}));
});
it('should not parse non-string inputs', function() {
expectParse(123456, 'dd.MM.yyyy', 123456);
var date = new Date();
expectParse(date, 'dd.MM.yyyy', date);
});
it('should not parse if no format is specified', function() {
expectParse('21.08.1951', '', '21.08.1951');
});
it('should reinitialize when locale changes', inject(function($locale) {
spyOn(dateParser, 'init').and.callThrough();
expect($locale.id).toBe('en-us');
$locale.id = 'en-uk';
dateParser.parse('22.March.15.22', 'd.MMMM.yy.s');
expect(dateParser.init).toHaveBeenCalled();
}));
describe('timezone functions', function() {
describe('toTimezone', function() {
it('adjusts date: PST - EST', function() {
var date = new Date('2008-01-01T00:00:00.000Z');
var toWestDate = dateParser.toTimezone(date, 'PST');
var toEastDate = dateParser.toTimezone(date, 'EST');
expect(toWestDate.getTime() - toEastDate.getTime()).toEqual(1000 * 60 * 60 * 3);
});
it('adjusts date: GMT-0500 - GMT+0500', function() {
var date = new Date('2008-01-01T00:00:00.000Z');
var toWestDate = dateParser.toTimezone(date, 'GMT-0500');
var toEastDate = dateParser.toTimezone(date, 'GMT+0500');
expect(toWestDate.getTime() - toEastDate.getTime()).toEqual(1000 * 60 * 60 * 10);
});
it('adjusts date: -600 - +600', function() {
var date = new Date('2008-01-01T00:00:00.000Z');
var toWestDate = dateParser.toTimezone(date, '-600');
var toEastDate = dateParser.toTimezone(date, '+600');
expect(toWestDate.getTime() - toEastDate.getTime()).toEqual(1000 * 60 * 60 * 12);
});
it('tolerates null date', function() {
var date = null;
var toNullDate = dateParser.toTimezone(date, '-600');
expect(toNullDate).toEqual(date);
});
it('tolerates null timezone', function() {
var date = new Date('2008-01-01T00:00:00.000Z');
var toNullTimezoneDate = dateParser.toTimezone(date, null);
expect(toNullTimezoneDate).toEqual(date);
});
});
describe('fromTimezone', function() {
it('adjusts date: PST - EST', function() {
var date = new Date('2008-01-01T00:00:00.000Z');
var fromWestDate = dateParser.fromTimezone(date, 'PST');
var fromEastDate = dateParser.fromTimezone(date, 'EST');
expect(fromWestDate.getTime() - fromEastDate.getTime()).toEqual(1000 * 60 * 60 * -3);
});
it('adjusts date: GMT-0500 - GMT+0500', function() {
var date = new Date('2008-01-01T00:00:00.000Z');
var fromWestDate = dateParser.fromTimezone(date, 'GMT-0500');
var fromEastDate = dateParser.fromTimezone(date, 'GMT+0500');
expect(fromWestDate.getTime() - fromEastDate.getTime()).toEqual(1000 * 60 * 60 * -10);
});
it('adjusts date: -600 - +600', function() {
var date = new Date('2008-01-01T00:00:00.000Z');
var fromWestDate = dateParser.fromTimezone(date, '-600');
var fromEastDate = dateParser.fromTimezone(date, '+600');
expect(fromWestDate.getTime() - fromEastDate.getTime()).toEqual(1000 * 60 * 60 * -12);
});
it('tolerates null date', function() {
var date = null;
var toNullDate = dateParser.fromTimezone(date, '-600');
expect(toNullDate).toEqual(date);
});
it('tolerates null timezone', function() {
var date = new Date('2008-01-01T00:00:00.000Z');
var toNullTimezoneDate = dateParser.fromTimezone(date, null);
expect(toNullTimezoneDate).toEqual(date);
});
});
describe('timezoneToOffset', function() {
it('calculates minutes off from current timezone', function() {
var offsetMinutesUtc = dateParser.timezoneToOffset('UTC');
var offsetMinutesUtcPlus1 = dateParser.timezoneToOffset('GMT+0100');
expect(offsetMinutesUtc - offsetMinutesUtcPlus1).toEqual(60);
});
});
describe('addDateMinutes', function() {
it('adds minutes to a date', function() {
var date = new Date('2008-01-01T00:00:00.000Z');
var oneHourMore = dateParser.addDateMinutes(date, 60);
expect(oneHourMore).toEqual(new Date('2008-01-01T01:00:00.000Z'));
});
});
describe('convertTimezoneToLocal', function() {
it('adjusts date: PST - EST', function() {
var date = new Date('2008-01-01T00:00:00.000Z');
var toWestDate = dateParser.convertTimezoneToLocal(date, 'PST');
var toEastDate = dateParser.convertTimezoneToLocal(date, 'EST');
expect(toWestDate.getTime() - toEastDate.getTime()).toEqual(1000 * 60 * 60 * 3);
});
it('adjusts date: GMT-0500 - GMT+0500', function() {
var date = new Date('2008-01-01T00:00:00.000Z');
var toWestDate = dateParser.convertTimezoneToLocal(date, 'GMT-0500');
var toEastDate = dateParser.convertTimezoneToLocal(date, 'GMT+0500');
expect(toWestDate.getTime() - toEastDate.getTime()).toEqual(1000 * 60 * 60 * 10);
});
it('adjusts date: -600 - +600', function() {
var date = new Date('2008-01-01T00:00:00.000Z');
var toWestDate = dateParser.convertTimezoneToLocal(date, '-600');
var toEastDate = dateParser.convertTimezoneToLocal(date, '+600');
expect(toWestDate.getTime() - toEastDate.getTime()).toEqual(1000 * 60 * 60 * 12);
});
});
});
describe('overrideParser', function() {
var twoDigitYearParser = function (value) {
this.year = +value + (+value > 30 ? 1900 : 2000);
};
it('should get the current parser', function() {
expect(dateParser.getParser('yy')).toBeTruthy();
});
it('should override the parser', function() {
dateParser.overrideParser('yy', twoDigitYearParser);
expect(dateParser.parse('68', 'yy').getFullYear()).toEqual(1968);
expect(dateParser.parse('67', 'yy').getFullYear()).toEqual(1967);
expect(dateParser.parse('31', 'yy').getFullYear()).toEqual(1931);
expect(dateParser.parse('30', 'yy').getFullYear()).toEqual(2030);
});
it('should clear cached parsers', function() {
expect(dateParser.parse('68', 'yy').getFullYear()).toEqual(2068);
dateParser.overrideParser('yy', twoDigitYearParser);
expect(dateParser.parse('68', 'yy').getFullYear()).toEqual(1968);
});
});
});
================================================
FILE: src/datepicker/datepicker.css
================================================
.uib-datepicker .uib-title {
width: 100%;
}
.uib-day button, .uib-month button, .uib-year button {
min-width: 100%;
}
.uib-left, .uib-right {
width: 100%
}
================================================
FILE: src/datepicker/datepicker.js
================================================
angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootstrap.isClass'])
.value('$datepickerSuppressError', false)
.value('$datepickerLiteralWarning', true)
.constant('uibDatepickerConfig', {
datepickerMode: 'day',
formatDay: 'dd',
formatMonth: 'MMMM',
formatYear: 'yyyy',
formatDayHeader: 'EEE',
formatDayTitle: 'MMMM yyyy',
formatMonthTitle: 'yyyy',
maxDate: null,
maxMode: 'year',
minDate: null,
minMode: 'day',
monthColumns: 3,
ngModelOptions: {},
shortcutPropagation: false,
showWeeks: true,
yearColumns: 5,
yearRows: 4
})
.controller('UibDatepickerController', ['$scope', '$element', '$attrs', '$parse', '$interpolate', '$locale', '$log', 'dateFilter', 'uibDatepickerConfig', '$datepickerLiteralWarning', '$datepickerSuppressError', 'uibDateParser',
function($scope, $element, $attrs, $parse, $interpolate, $locale, $log, dateFilter, datepickerConfig, $datepickerLiteralWarning, $datepickerSuppressError, dateParser) {
var self = this,
ngModelCtrl = { $setViewValue: angular.noop }, // nullModelCtrl;
ngModelOptions = {},
watchListeners = [];
$element.addClass('uib-datepicker');
$attrs.$set('role', 'application');
if (!$scope.datepickerOptions) {
$scope.datepickerOptions = {};
}
// Modes chain
this.modes = ['day', 'month', 'year'];
[
'customClass',
'dateDisabled',
'datepickerMode',
'formatDay',
'formatDayHeader',
'formatDayTitle',
'formatMonth',
'formatMonthTitle',
'formatYear',
'maxDate',
'maxMode',
'minDate',
'minMode',
'monthColumns',
'showWeeks',
'shortcutPropagation',
'startingDay',
'yearColumns',
'yearRows'
].forEach(function(key) {
switch (key) {
case 'customClass':
case 'dateDisabled':
$scope[key] = $scope.datepickerOptions[key] || angular.noop;
break;
case 'datepickerMode':
$scope.datepickerMode = angular.isDefined($scope.datepickerOptions.datepickerMode) ?
$scope.datepickerOptions.datepickerMode : datepickerConfig.datepickerMode;
break;
case 'formatDay':
case 'formatDayHeader':
case 'formatDayTitle':
case 'formatMonth':
case 'formatMonthTitle':
case 'formatYear':
self[key] = angular.isDefined($scope.datepickerOptions[key]) ?
$interpolate($scope.datepickerOptions[key])($scope.$parent) :
datepickerConfig[key];
break;
case 'monthColumns':
case 'showWeeks':
case 'shortcutPropagation':
case 'yearColumns':
case 'yearRows':
self[key] = angular.isDefined($scope.datepickerOptions[key]) ?
$scope.datepickerOptions[key] : datepickerConfig[key];
break;
case 'startingDay':
if (angular.isDefined($scope.datepickerOptions.startingDay)) {
self.startingDay = $scope.datepickerOptions.startingDay;
} else if (angular.isNumber(datepickerConfig.startingDay)) {
self.startingDay = datepickerConfig.startingDay;
} else {
self.startingDay = ($locale.DATETIME_FORMATS.FIRSTDAYOFWEEK + 8) % 7;
}
break;
case 'maxDate':
case 'minDate':
$scope.$watch('datepickerOptions.' + key, function(value) {
if (value) {
if (angular.isDate(value)) {
self[key] = dateParser.fromTimezone(new Date(value), ngModelOptions.getOption('timezone'));
} else {
if ($datepickerLiteralWarning) {
$log.warn('Literal date support has been deprecated, please switch to date object usage');
}
self[key] = new Date(dateFilter(value, 'medium'));
}
} else {
self[key] = datepickerConfig[key] ?
dateParser.fromTimezone(new Date(datepickerConfig[key]), ngModelOptions.getOption('timezone')) :
null;
}
self.refreshView();
});
break;
case 'maxMode':
case 'minMode':
if ($scope.datepickerOptions[key]) {
$scope.$watch(function() { return $scope.datepickerOptions[key]; }, function(value) {
self[key] = $scope[key] = angular.isDefined(value) ? value : $scope.datepickerOptions[key];
if (key === 'minMode' && self.modes.indexOf($scope.datepickerOptions.datepickerMode) < self.modes.indexOf(self[key]) ||
key === 'maxMode' && self.modes.indexOf($scope.datepickerOptions.datepickerMode) > self.modes.indexOf(self[key])) {
$scope.datepickerMode = self[key];
$scope.datepickerOptions.datepickerMode = self[key];
}
});
} else {
self[key] = $scope[key] = datepickerConfig[key] || null;
}
break;
}
});
$scope.uniqueId = 'datepicker-' + $scope.$id + '-' + Math.floor(Math.random() * 10000);
$scope.disabled = angular.isDefined($attrs.disabled) || false;
if (angular.isDefined($attrs.ngDisabled)) {
watchListeners.push($scope.$parent.$watch($attrs.ngDisabled, function(disabled) {
$scope.disabled = disabled;
self.refreshView();
}));
}
$scope.isActive = function(dateObject) {
if (self.compare(dateObject.date, self.activeDate) === 0) {
$scope.activeDateId = dateObject.uid;
return true;
}
return false;
};
this.init = function(ngModelCtrl_) {
ngModelCtrl = ngModelCtrl_;
ngModelOptions = extractOptions(ngModelCtrl);
if ($scope.datepickerOptions.initDate) {
self.activeDate = dateParser.fromTimezone($scope.datepickerOptions.initDate, ngModelOptions.getOption('timezone')) || new Date();
$scope.$watch('datepickerOptions.initDate', function(initDate) {
if (initDate && (ngModelCtrl.$isEmpty(ngModelCtrl.$modelValue) || ngModelCtrl.$invalid)) {
self.activeDate = dateParser.fromTimezone(initDate, ngModelOptions.getOption('timezone'));
self.refreshView();
}
});
} else {
self.activeDate = new Date();
}
var date = ngModelCtrl.$modelValue ? new Date(ngModelCtrl.$modelValue) : new Date();
this.activeDate = !isNaN(date) ?
dateParser.fromTimezone(date, ngModelOptions.getOption('timezone')) :
dateParser.fromTimezone(new Date(), ngModelOptions.getOption('timezone'));
ngModelCtrl.$render = function() {
self.render();
};
};
this.render = function() {
if (ngModelCtrl.$viewValue) {
var date = new Date(ngModelCtrl.$viewValue),
isValid = !isNaN(date);
if (isValid) {
this.activeDate = dateParser.fromTimezone(date, ngModelOptions.getOption('timezone'));
} else if (!$datepickerSuppressError) {
$log.error('Datepicker directive: "ng-model" value must be a Date object');
}
}
this.refreshView();
};
this.refreshView = function() {
if (this.element) {
$scope.selectedDt = null;
this._refreshView();
if ($scope.activeDt) {
$scope.activeDateId = $scope.activeDt.uid;
}
var date = ngModelCtrl.$viewValue ? new Date(ngModelCtrl.$viewValue) : null;
date = dateParser.fromTimezone(date, ngModelOptions.getOption('timezone'));
ngModelCtrl.$setValidity('dateDisabled', !date ||
this.element && !this.isDisabled(date));
}
};
this.createDateObject = function(date, format) {
var model = ngModelCtrl.$viewValue ? new Date(ngModelCtrl.$viewValue) : null;
model = dateParser.fromTimezone(model, ngModelOptions.getOption('timezone'));
var today = new Date();
today = dateParser.fromTimezone(today, ngModelOptions.getOption('timezone'));
var time = this.compare(date, today);
var dt = {
date: date,
label: dateParser.filter(date, format),
selected: model && this.compare(date, model) === 0,
disabled: this.isDisabled(date),
past: time < 0,
current: time === 0,
future: time > 0,
customClass: this.customClass(date) || null
};
if (model && this.compare(date, model) === 0) {
$scope.selectedDt = dt;
}
if (self.activeDate && this.compare(dt.date, self.activeDate) === 0) {
$scope.activeDt = dt;
}
return dt;
};
this.isDisabled = function(date) {
return $scope.disabled ||
this.minDate && this.compare(date, this.minDate) < 0 ||
this.maxDate && this.compare(date, this.maxDate) > 0 ||
$scope.dateDisabled && $scope.dateDisabled({date: date, mode: $scope.datepickerMode});
};
this.customClass = function(date) {
return $scope.customClass({date: date, mode: $scope.datepickerMode});
};
// Split array into smaller arrays
this.split = function(arr, size) {
var arrays = [];
while (arr.length > 0) {
arrays.push(arr.splice(0, size));
}
return arrays;
};
$scope.select = function(date) {
if ($scope.datepickerMode === self.minMode) {
var dt = ngModelCtrl.$viewValue ? dateParser.fromTimezone(new Date(ngModelCtrl.$viewValue), ngModelOptions.getOption('timezone')) : new Date(0, 0, 0, 0, 0, 0, 0);
dt.setFullYear(date.getFullYear(), date.getMonth(), date.getDate());
dt = dateParser.toTimezone(dt, ngModelOptions.getOption('timezone'));
ngModelCtrl.$setViewValue(dt);
ngModelCtrl.$render();
} else {
self.activeDate = date;
setMode(self.modes[self.modes.indexOf($scope.datepickerMode) - 1]);
$scope.$emit('uib:datepicker.mode');
}
$scope.$broadcast('uib:datepicker.focus');
};
$scope.move = function(direction) {
var year = self.activeDate.getFullYear() + direction * (self.step.years || 0),
month = self.activeDate.getMonth() + direction * (self.step.months || 0);
self.activeDate.setFullYear(year, month, 1);
self.refreshView();
};
$scope.toggleMode = function(direction) {
direction = direction || 1;
if ($scope.datepickerMode === self.maxMode && direction === 1 ||
$scope.datepickerMode === self.minMode && direction === -1) {
return;
}
setMode(self.modes[self.modes.indexOf($scope.datepickerMode) + direction]);
$scope.$emit('uib:datepicker.mode');
};
// Key event mapper
$scope.keys = { 13: 'enter', 32: 'space', 33: 'pageup', 34: 'pagedown', 35: 'end', 36: 'home', 37: 'left', 38: 'up', 39: 'right', 40: 'down' };
var focusElement = function() {
self.element[0].focus();
};
// Listen for focus requests from popup directive
$scope.$on('uib:datepicker.focus', focusElement);
$scope.keydown = function(evt) {
var key = $scope.keys[evt.which];
if (!key || evt.shiftKey || evt.altKey || $scope.disabled) {
return;
}
evt.preventDefault();
if (!self.shortcutPropagation) {
evt.stopPropagation();
}
if (key === 'enter' || key === 'space') {
if (self.isDisabled(self.activeDate)) {
return; // do nothing
}
$scope.select(self.activeDate);
} else if (evt.ctrlKey && (key === 'up' || key === 'down')) {
$scope.toggleMode(key === 'up' ? 1 : -1);
} else {
self.handleKeyDown(key, evt);
self.refreshView();
}
};
$element.on('keydown', function(evt) {
$scope.$apply(function() {
$scope.keydown(evt);
});
});
$scope.$on('$destroy', function() {
//Clear all watch listeners on destroy
while (watchListeners.length) {
watchListeners.shift()();
}
});
function setMode(mode) {
$scope.datepickerMode = mode;
$scope.datepickerOptions.datepickerMode = mode;
}
function extractOptions(ngModelCtrl) {
var ngModelOptions;
if (angular.version.minor < 6) { // in angular < 1.6 $options could be missing
// guarantee a value
ngModelOptions = ngModelCtrl.$options ||
$scope.datepickerOptions.ngModelOptions ||
datepickerConfig.ngModelOptions ||
{};
// mimic 1.6+ api
ngModelOptions.getOption = function (key) {
return ngModelOptions[key];
};
} else { // in angular >=1.6 $options is always present
// ng-model-options defaults timezone to null; don't let its precedence squash a non-null value
var timezone = ngModelCtrl.$options.getOption('timezone') ||
($scope.datepickerOptions.ngModelOptions ? $scope.datepickerOptions.ngModelOptions.timezone : null) ||
(datepickerConfig.ngModelOptions ? datepickerConfig.ngModelOptions.timezone : null);
// values passed to createChild override existing values
ngModelOptions = ngModelCtrl.$options // start with a ModelOptions instance
.createChild(datepickerConfig.ngModelOptions) // lowest precedence
.createChild($scope.datepickerOptions.ngModelOptions)
.createChild(ngModelCtrl.$options) // highest precedence
.createChild({timezone: timezone}); // to keep from squashing a non-null value
}
return ngModelOptions;
}
}])
.controller('UibDaypickerController', ['$scope', '$element', 'dateFilter', function(scope, $element, dateFilter) {
var DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
this.step = { months: 1 };
this.element = $element;
function getDaysInMonth(year, month) {
return month === 1 && year % 4 === 0 &&
(year % 100 !== 0 || year % 400 === 0) ? 29 : DAYS_IN_MONTH[month];
}
this.init = function(ctrl) {
angular.extend(ctrl, this);
scope.showWeeks = ctrl.showWeeks;
ctrl.refreshView();
};
this.getDates = function(startDate, n) {
var dates = new Array(n), current = new Date(startDate), i = 0, date;
while (i < n) {
date = new Date(current);
dates[i++] = date;
current.setDate(current.getDate() + 1);
}
return dates;
};
this._refreshView = function() {
var year = this.activeDate.getFullYear(),
month = this.activeDate.getMonth(),
firstDayOfMonth = new Date(this.activeDate);
firstDayOfMonth.setFullYear(year, month, 1);
var difference = this.startingDay - firstDayOfMonth.getDay(),
numDisplayedFromPreviousMonth = difference > 0 ?
7 - difference : - difference,
firstDate = new Date(firstDayOfMonth);
if (numDisplayedFromPreviousMonth > 0) {
firstDate.setDate(-numDisplayedFromPreviousMonth + 1);
}
// 42 is the number of days on a six-week calendar
var days = this.getDates(firstDate, 42);
for (var i = 0; i < 42; i ++) {
days[i] = angular.extend(this.createDateObject(days[i], this.formatDay), {
secondary: days[i].getMonth() !== month,
uid: scope.uniqueId + '-' + i
});
}
scope.labels = new Array(7);
for (var j = 0; j < 7; j++) {
scope.labels[j] = {
abbr: dateFilter(days[j].date, this.formatDayHeader),
full: dateFilter(days[j].date, 'EEEE')
};
}
scope.title = dateFilter(this.activeDate, this.formatDayTitle);
scope.rows = this.split(days, 7);
if (scope.showWeeks) {
scope.weekNumbers = [];
var thursdayIndex = (4 + 7 - this.startingDay) % 7,
numWeeks = scope.rows.length;
for (var curWeek = 0; curWeek < numWeeks; curWeek++) {
scope.weekNumbers.push(
getISO8601WeekNumber(scope.rows[curWeek][thursdayIndex].date));
}
}
};
this.compare = function(date1, date2) {
var _date1 = new Date(date1.getFullYear(), date1.getMonth(), date1.getDate());
var _date2 = new Date(date2.getFullYear(), date2.getMonth(), date2.getDate());
_date1.setFullYear(date1.getFullYear());
_date2.setFullYear(date2.getFullYear());
return _date1 - _date2;
};
function getISO8601WeekNumber(date) {
var checkDate = new Date(date);
checkDate.setDate(checkDate.getDate() + 4 - (checkDate.getDay() || 7)); // Thursday
var time = checkDate.getTime();
checkDate.setMonth(0); // Compare with Jan 1
checkDate.setDate(1);
return Math.floor(Math.round((time - checkDate) / 86400000) / 7) + 1;
}
this.handleKeyDown = function(key, evt) {
var date = this.activeDate.getDate();
if (key === 'left') {
date = date - 1;
} else if (key === 'up') {
date = date - 7;
} else if (key === 'right') {
date = date + 1;
} else if (key === 'down') {
date = date + 7;
} else if (key === 'pageup' || key === 'pagedown') {
var month = this.activeDate.getMonth() + (key === 'pageup' ? - 1 : 1);
this.activeDate.setMonth(month, 1);
date = Math.min(getDaysInMonth(this.activeDate.getFullYear(), this.activeDate.getMonth()), date);
} else if (key === 'home') {
date = 1;
} else if (key === 'end') {
date = getDaysInMonth(this.activeDate.getFullYear(), this.activeDate.getMonth());
}
this.activeDate.setDate(date);
};
}])
.controller('UibMonthpickerController', ['$scope', '$element', 'dateFilter', function(scope, $element, dateFilter) {
this.step = { years: 1 };
this.element = $element;
this.init = function(ctrl) {
angular.extend(ctrl, this);
ctrl.refreshView();
};
this._refreshView = function() {
var months = new Array(12),
year = this.activeDate.getFullYear(),
date;
for (var i = 0; i < 12; i++) {
date = new Date(this.activeDate);
date.setFullYear(year, i, 1);
months[i] = angular.extend(this.createDateObject(date, this.formatMonth), {
uid: scope.uniqueId + '-' + i
});
}
scope.title = dateFilter(this.activeDate, this.formatMonthTitle);
scope.rows = this.split(months, this.monthColumns);
scope.yearHeaderColspan = this.monthColumns > 3 ? this.monthColumns - 2 : 1;
};
this.compare = function(date1, date2) {
var _date1 = new Date(date1.getFullYear(), date1.getMonth());
var _date2 = new Date(date2.getFullYear(), date2.getMonth());
_date1.setFullYear(date1.getFullYear());
_date2.setFullYear(date2.getFullYear());
return _date1 - _date2;
};
this.handleKeyDown = function(key, evt) {
var date = this.activeDate.getMonth();
if (key === 'left') {
date = date - 1;
} else if (key === 'up') {
date = date - this.monthColumns;
} else if (key === 'right') {
date = date + 1;
} else if (key === 'down') {
date = date + this.monthColumns;
} else if (key === 'pageup' || key === 'pagedown') {
var year = this.activeDate.getFullYear() + (key === 'pageup' ? - 1 : 1);
this.activeDate.setFullYear(year);
} else if (key === 'home') {
date = 0;
} else if (key === 'end') {
date = 11;
}
this.activeDate.setMonth(date);
};
}])
.controller('UibYearpickerController', ['$scope', '$element', 'dateFilter', function(scope, $element, dateFilter) {
var columns, range;
this.element = $element;
function getStartingYear(year) {
return parseInt((year - 1) / range, 10) * range + 1;
}
this.yearpickerInit = function() {
columns = this.yearColumns;
range = this.yearRows * columns;
this.step = { years: range };
};
this._refreshView = function() {
var years = new Array(range), date;
for (var i = 0, start = getStartingYear(this.activeDate.getFullYear()); i < range; i++) {
date = new Date(this.activeDate);
date.setFullYear(start + i, 0, 1);
years[i] = angular.extend(this.createDateObject(date, this.formatYear), {
uid: scope.uniqueId + '-' + i
});
}
scope.title = [years[0].label, years[range - 1].label].join(' - ');
scope.rows = this.split(years, columns);
scope.columns = columns;
};
this.compare = function(date1, date2) {
return date1.getFullYear() - date2.getFullYear();
};
this.handleKeyDown = function(key, evt) {
var date = this.activeDate.getFullYear();
if (key === 'left') {
date = date - 1;
} else if (key === 'up') {
date = date - columns;
} else if (key === 'right') {
date = date + 1;
} else if (key === 'down') {
date = date + columns;
} else if (key === 'pageup' || key === 'pagedown') {
date += (key === 'pageup' ? - 1 : 1) * range;
} else if (key === 'home') {
date = getStartingYear(this.activeDate.getFullYear());
} else if (key === 'end') {
date = getStartingYear(this.activeDate.getFullYear()) + range - 1;
}
this.activeDate.setFullYear(date);
};
}])
.directive('uibDatepicker', function() {
return {
templateUrl: function(element, attrs) {
return attrs.templateUrl || 'uib/template/datepicker/datepicker.html';
},
scope: {
datepickerOptions: '=?'
},
require: ['uibDatepicker', '^ngModel'],
restrict: 'A',
controller: 'UibDatepickerController',
controllerAs: 'datepicker',
link: function(scope, element, attrs, ctrls) {
var datepickerCtrl = ctrls[0], ngModelCtrl = ctrls[1];
datepickerCtrl.init(ngModelCtrl);
}
};
})
.directive('uibDaypicker', function() {
return {
templateUrl: function(element, attrs) {
return attrs.templateUrl || 'uib/template/datepicker/day.html';
},
require: ['^uibDatepicker', 'uibDaypicker'],
restrict: 'A',
controller: 'UibDaypickerController',
link: function(scope, element, attrs, ctrls) {
var datepickerCtrl = ctrls[0],
daypickerCtrl = ctrls[1];
daypickerCtrl.init(datepickerCtrl);
}
};
})
.directive('uibMonthpicker', function() {
return {
templateUrl: function(element, attrs) {
return attrs.templateUrl || 'uib/template/datepicker/month.html';
},
require: ['^uibDatepicker', 'uibMonthpicker'],
restrict: 'A',
controller: 'UibMonthpickerController',
link: function(scope, element, attrs, ctrls) {
var datepickerCtrl = ctrls[0],
monthpickerCtrl = ctrls[1];
monthpickerCtrl.init(datepickerCtrl);
}
};
})
.directive('uibYearpicker', function() {
return {
templateUrl: function(element, attrs) {
return attrs.templateUrl || 'uib/template/datepicker/year.html';
},
require: ['^uibDatepicker', 'uibYearpicker'],
restrict: 'A',
controller: 'UibYearpickerController',
link: function(scope, element, attrs, ctrls) {
var ctrl = ctrls[0];
angular.extend(ctrl, ctrls[1]);
ctrl.yearpickerInit();
ctrl.refreshView();
}
};
});
================================================
FILE: src/datepicker/docs/demo.html
================================================
Selected date is: {{dt | date:'fullDate' }}
Inline
================================================
FILE: src/datepicker/docs/demo.js
================================================
angular.module('ui.bootstrap.demo').controller('DatepickerDemoCtrl', function ($scope) {
$scope.today = function() {
$scope.dt = new Date();
};
$scope.today();
$scope.clear = function() {
$scope.dt = null;
};
$scope.options = {
customClass: getDayClass,
minDate: new Date(),
showWeeks: true
};
// Disable weekend selection
function disabled(data) {
var date = data.date,
mode = data.mode;
return mode === 'day' && (date.getDay() === 0 || date.getDay() === 6);
}
$scope.toggleMin = function() {
$scope.options.minDate = $scope.options.minDate ? null : new Date();
};
$scope.toggleMin();
$scope.setDate = function(year, month, day) {
$scope.dt = new Date(year, month, day);
};
var tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
var afterTomorrow = new Date(tomorrow);
afterTomorrow.setDate(tomorrow.getDate() + 1);
$scope.events = [
{
date: tomorrow,
status: 'full'
},
{
date: afterTomorrow,
status: 'partially'
}
];
function getDayClass(data) {
var date = data.date,
mode = data.mode;
if (mode === 'day') {
var dayToCheck = new Date(date).setHours(0,0,0,0);
for (var i = 0; i < $scope.events.length; i++) {
var currentDay = new Date($scope.events[i].date).setHours(0,0,0,0);
if (dayToCheck === currentDay) {
return $scope.events[i].status;
}
}
}
return '';
}
});
================================================
FILE: src/datepicker/docs/readme.md
================================================
Our datepicker is flexible and fully customizable.
You can navigate through days, months and years.
The datepicker has 3 modes:
* `day` - In this mode you're presented with a 6-week calendar for a specified month.
* `month` - In this mode you can select a month within a selected year.
* `year` - In this mode you are presented with a range of years (20 by default).
### uib-datepicker settings
* `ng-model`
$
-
The date object. Must be a Javascript `Date` object. You may use the `uibDateParser` service to assist in string-to-object conversion.
* `ng-model-options`
$
C
_(Default: `{}`)_ -
Supported [angular ngModelOptions](https://docs.angularjs.org/api/ng/directive/ngModelOptions):
* allowInvalid
* timezone
* `template-url`
_(Default: `uib/template/datepicker/datepicker.html`)_ -
Add the ability to override the template used on the component.
Apart from the previous settings, to configure the uib-datepicker you need to create an object in Javascript with all the options and use it on the `datepicker-options` attribute:
* `datepicker-options`
$ -
An object to configure the datepicker in one place.
* `customClass ({date: date, mode: mode})` -
An optional expression to add classes based on passing an object with date and current mode properties.
* `dateDisabled ({date: date, mode: mode})` -
An optional expression to disable visible options based on passing an object with date and current mode properties.
* `datepickerMode`
C
_(Default: `day`)_ -
Current mode of the datepicker _(day|month|year)_. Can be used to initialize the datepicker in a specific mode.
* `formatDay`
C
_(Default: `dd`)_ -
Format of day in month.
* `formatMonth`
C
_(Default: `MMMM`)_ -
Format of month in year.
* `formatYear`
C
_(Default: `yyyy`)_ -
Format of year in year range.
* `formatDayHeader`
C
_(Default: `EEE`)_ -
Format of day in week header.
* `formatDayTitle`
C
_(Default: `MMMM yyyy`)_ -
Format of title when selecting day.
* `formatMonthTitle`
C
_(Default: `yyyy`)_ -
Format of title when selecting month.
* `initDate`
_(Default: `null`)_ -
The initial date view when no model value is specified.
* `maxDate`
C
_(Default: `null`)_ -
Defines the maximum available date. Requires a Javascript Date object.
* `maxMode`
C
_(Default: `year`)_ -
Sets an upper limit for mode.
* `minDate`
C
_(Default: `null`)_ -
Defines the minimum available date. Requires a Javascript Date object.
* `minMode`
C
_(Default: `day`)_ -
Sets a lower limit for mode.
* `monthColumns`
C
_(Default: `3`)_ -
Number of columns displayed in month selection.
* `ngModelOptions`
C
_(Default: `null`)_ -
Sets `ngModelOptions` for datepicker. This can be overridden through attribute usage
* `shortcutPropagation`
C
_(Default: `false`)_ -
An option to disable the propagation of the keydown event.
* `showWeeks`
C
_(Default: `true`)_ -
Whether to display week numbers.
* `startingDay`
C
*(Default: `$locale.DATETIME_FORMATS.FIRSTDAYOFWEEK`)* -
Starting day of the week from 0-6 (0=Sunday, ..., 6=Saturday).
* `yearRows`
C
_(Default: `4`)_ -
Number of rows displayed in year selection.
* `yearColumns`
C
_(Default: `5`)_ -
Number of columns displayed in year selection.
### Keyboard support
Depending on datepicker's current mode, the date may refer either to day, month or year. Accordingly, the term view refers either to a month, year or year range.
* `Left`: Move focus to the previous date. Will move to the last date of the previous view, if the current date is the first date of a view.
* `Right`: Move focus to the next date. Will move to the first date of the following view, if the current date is the last date of a view.
* `Up`: Move focus to the same column of the previous row. Will wrap to the appropriate row in the previous view.
* `Down`: Move focus to the same column of the following row. Will wrap to the appropriate row in the following view.
* `PgUp`: Move focus to the same date of the previous view. If that date does not exist, focus is placed on the last date of the month.
* `PgDn`: Move focus to the same date of the following view. If that date does not exist, focus is placed on the last date of the month.
* `Home`: Move to the first date of the view.
* `End`: Move to the last date of the view.
* `Enter`/`Space`: Select date.
* `Ctrl`+`Up`: Move to an upper mode.
* `Ctrl`+`Down`: Move to a lower mode.
* `Esc`: Will close popup, and move focus to the input.
**Notes**
If the date a user enters falls outside of the min-/max-date range, a `dateDisabled` validation error will show on the form.
================================================
FILE: src/datepicker/index-nocss.js
================================================
require('../dateparser');
require('../isClass');
require('../../template/datepicker/datepicker.html.js');
require('../../template/datepicker/day.html.js');
require('../../template/datepicker/month.html.js');
require('../../template/datepicker/year.html.js');
require('./datepicker');
var MODULE_NAME = 'ui.bootstrap.module.datepicker';
angular.module(MODULE_NAME, ['ui.bootstrap.datepicker', 'uib/template/datepicker/datepicker.html', 'uib/template/datepicker/day.html', 'uib/template/datepicker/month.html', 'uib/template/datepicker/year.html']);
module.exports = MODULE_NAME;
================================================
FILE: src/datepicker/index.js
================================================
require('./datepicker.css');
module.exports = require('./index-nocss.js');
================================================
FILE: src/datepicker/test/datepicker.spec.js
================================================
describe('datepicker', function() {
var $rootScope, $compile, $templateCache, element;
beforeEach(module('ui.bootstrap.datepicker'));
beforeEach(module('uib/template/datepicker/datepicker.html'));
beforeEach(module('uib/template/datepicker/day.html'));
beforeEach(module('uib/template/datepicker/month.html'));
beforeEach(module('uib/template/datepicker/year.html'));
beforeEach(module(function($compileProvider) {
$compileProvider.directive('dateModel', function() {
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, element, attrs, modelController) {
modelController.$formatters.push(function(object) {
return new Date(object.date);
});
modelController.$parsers.push(function(date) {
return {
type: 'date',
date: date.toUTCString()
};
});
}
};
});
}));
function getTitleCell() {
return element.find('th').eq(1);
}
function getTitleButton() {
return getTitleCell().find('button').first();
}
function getTitle() {
return getTitleButton().text();
}
function clickTitleButton() {
getTitleButton().click();
}
function clickPreviousButton(times) {
var el = element.find('th').eq(0).find('button').eq(0);
for (var i = 0, n = times || 1; i < n; i++) {
el.click();
}
}
function clickNextButton() {
element.find('th').eq(2).find('button').eq(0).click();
}
function getLabelsRow() {
return element.find('thead').find('tr').eq(1);
}
function getLabels(dayMode) {
var els = getLabelsRow().find('th'),
labels = [];
for (var i = dayMode ? 1 : 0, n = els.length; i < n; i++) {
labels.push(els.eq(i).text());
}
return labels;
}
function getWeeks() {
var rows = element.find('tbody').find('tr'),
weeks = [];
for (var i = 0, n = rows.length; i < n; i++) {
weeks.push(rows.eq(i).find('td').eq(0).first().text());
}
return weeks;
}
function getOptions(dayMode) {
var tr = element.find('tbody').find('tr');
var rows = [];
for (var j = 0, numRows = tr.length; j < numRows; j++) {
var cols = tr.eq(j).find('td'), days = [];
for (var i = dayMode ? 1 : 0, n = cols.length; i < n; i++) {
days.push(cols.eq(i).find('button').text());
}
rows.push(days);
}
return rows;
}
function clickOption(index) {
getAllOptionsEl().eq(index).click();
}
function getAllOptionsEl(dayMode) {
return element.find('tbody').find('button');
}
function selectedElementIndex() {
var buttons = getAllOptionsEl();
for (var i = 0; i < buttons.length; i++) {
if (angular.element(buttons[i]).hasClass('btn-info')) {
return i;
}
}
}
function expectSelectedElement(index) {
var buttons = getAllOptionsEl();
angular.forEach( buttons, function(button, idx) {
expect(angular.element(button).hasClass('btn-info')).toBe(idx === index);
});
}
function getSelectedElement(index) {
var buttons = getAllOptionsEl();
var el = $.grep(buttons, function(button, idx) {
return angular.element(button).hasClass('btn-info');
})[0];
return angular.element(el);
}
function triggerKeyDown(element, key, ctrl) {
var keyCodes = {
'enter': 13,
'space': 32,
'pageup': 33,
'pagedown': 34,
'end': 35,
'home': 36,
'left': 37,
'up': 38,
'right': 39,
'down': 40,
'esc': 27
};
var e = $.Event('keydown');
e.which = keyCodes[key];
if (ctrl) {
e.ctrlKey = true;
}
element.trigger(e);
}
describe('$datepickerLiteralWarning', function() {
var $compile,
$log,
$scope;
it('should warn when using literals for min date by default', function() {
inject(function(_$log_, _$rootScope_, _$compile_) {
$log = _$log_;
$scope = _$rootScope_.$new();
$compile = _$compile_;
});
spyOn($log, 'warn');
$scope.options = {
minDate: '1984-01-01'
};
element = $compile('')($scope);
$scope.$digest();
expect($log.warn).toHaveBeenCalledWith('Literal date support has been deprecated, please switch to date object usage');
});
it('should suppress warning when using literals for min date', function() {
module(function($provide) {
$provide.value('$datepickerLiteralWarning', false);
});
inject(function(_$log_, _$rootScope_, _$compile_) {
$log = _$log_;
$scope = _$rootScope_.$new();
$compile = _$compile_;
});
spyOn($log, 'warn');
$scope.options = {
minDate: '1984-01-01'
};
element = $compile('')($scope);
$scope.$digest();
expect($log.warn).not.toHaveBeenCalled();
});
it('should warn when using literals for max date by default', function() {
inject(function(_$log_, _$rootScope_, _$compile_) {
$log = _$log_;
$scope = _$rootScope_.$new();
$compile = _$compile_;
});
spyOn($log, 'warn');
$scope.options = {
maxDate: '1984-01-01'
};
element = $compile('')($scope);
$scope.$digest();
expect($log.warn).toHaveBeenCalledWith('Literal date support has been deprecated, please switch to date object usage');
});
it('should suppress warning when using literals for max date', function() {
module(function($provide) {
$provide.value('$datepickerLiteralWarning', false);
});
inject(function(_$log_, _$rootScope_, _$compile_) {
$log = _$log_;
$scope = _$rootScope_.$new();
$compile = _$compile_;
});
spyOn($log, 'warn');
$scope.options = {
maxDate: '1984-01-01'
};
element = $compile('')($scope);
$scope.$digest();
expect($log.warn).not.toHaveBeenCalled();
});
});
describe('$datepickerSuppressError', function() {
var $compile,
$log,
$scope;
it('should not suppress log error message for ng-model date error by default', function() {
inject(function(_$log_, _$rootScope_, _$compile_) {
$log = _$log_;
$scope = _$rootScope_.$new();
$compile = _$compile_;
});
spyOn($log, 'error');
element = $compile('')($scope);
$scope.locals = {
date: 'lalala'
};
$scope.$digest();
expect($log.error).toHaveBeenCalled();
});
it('should not suppress log error message for ng-model date error when false', function() {
module(function($provide) {
$provide.value('$datepickerSuppressError', false);
});
inject(function(_$log_, _$rootScope_, _$compile_) {
$log = _$log_;
$scope = _$rootScope_.$new();
$compile = _$compile_;
});
spyOn($log, 'error');
element = $compile('')($scope);
$scope.locals = {
date: 'lalala'
};
$scope.$digest();
expect($log.error).toHaveBeenCalled();
});
it('should suppress log error message for ng-model date error when true', function() {
module(function($provide) {
$provide.value('$datepickerSuppressError', true);
});
inject(function(_$log_, _$rootScope_, _$compile_) {
$log = _$log_;
$scope = _$rootScope_.$new();
$compile = _$compile_;
});
spyOn($log, 'error');
element = $compile('')($scope);
$scope.locals = {
date: 'lalala'
};
$scope.$digest();
expect($log.error).not.toHaveBeenCalled();
});
});
describe('', function() {
beforeEach(inject(function(_$compile_, _$rootScope_, _$templateCache_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
$rootScope.date = new Date('September 30, 2010 15:30:00');
$templateCache = _$templateCache_;
}));
describe('with no initial date', function() {
beforeEach(function() {
jasmine.clock().install();
});
afterEach(function() {
jasmine.clock().uninstall();
});
it('should have an active date equal to the current date', function() {
var baseTime = new Date(2015, 2, 23);
jasmine.clock().mockDate(baseTime);
element = $compile('baz');
element = $compile('')($rootScope);
$rootScope.$digest();
expect(element.html()).toBe('baz
');
});
it('should support custom day, month and year templates', function() {
$templateCache.put('foo/day.html', 'day
');
$templateCache.put('foo/month.html', 'month
');
$templateCache.put('foo/year.html', 'year
');
$templateCache.put('foo/bar.html', '');
element = $compile('')($rootScope);
$rootScope.$digest();
var expectedHtml = '';
expect(element.html()).toBe(expectedHtml);
});
it('should expose the controller in the template', function() {
$templateCache.put('uib/template/datepicker/datepicker.html', '{{datepicker.text}}
');
element = $compile('')($rootScope);
$rootScope.$digest();
var ctrl = element.controller('uib-datepicker');
expect(ctrl).toBeDefined();
expect(element.html()).toBe('');
ctrl.text = 'baz';
$rootScope.$digest();
expect(element.html()).toBe('baz
');
});
describe('basic functionality', function() {
beforeEach(function() {
element = $compile('')($rootScope);
$rootScope.$digest();
});
it('is has a `` element', function() {
expect(element.find('table').length).toBe(1);
});
it('shows the correct title', function() {
expect(getTitle()).toBe('September 2010');
});
it('shows the label row & the correct day labels', function() {
expect(getLabelsRow().css('display')).not.toBe('none');
expect(getLabels(true)).toEqual(['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']);
});
it('renders the calendar days correctly', function() {
expect(getOptions(true)).toEqual([
['29', '30', '31', '01', '02', '03', '04'],
['05', '06', '07', '08', '09', '10', '11'],
['12', '13', '14', '15', '16', '17', '18'],
['19', '20', '21', '22', '23', '24', '25'],
['26', '27', '28', '29', '30', '01', '02'],
['03', '04', '05', '06', '07', '08', '09']
]);
});
it('renders the week numbers based on ISO 8601', function() {
expect(getWeeks()).toEqual(['35', '36', '37', '38', '39', '40']);
});
it('value is correct', function() {
expect($rootScope.date).toEqual(new Date('September 30, 2010 15:30:00'));
});
it('has activeDate value of model', function() {
expect(element.controller('uibDatepicker').activeDate).toEqual(new Date('September 30, 2010 15:30:00'));
});
it('has `selected` only the correct day', function() {
expectSelectedElement(32);
});
it('has no `selected` day when model is cleared', function() {
$rootScope.date = null;
$rootScope.$digest();
expect($rootScope.date).toBe(null);
expectSelectedElement(null);
});
it('does not change current view when model is cleared', function() {
$rootScope.date = null;
$rootScope.$digest();
expect($rootScope.date).toBe(null);
expect(getTitle()).toBe('September 2010');
});
it('`disables` visible dates from other months', function() {
var buttons = getAllOptionsEl();
angular.forEach(buttons, function(button, index) {
expect(angular.element(button).find('span').hasClass('text-muted')).toBe( index < 3 || index > 32 );
});
});
it('updates the model when a day is clicked', function() {
clickOption(17);
expect($rootScope.date).toEqual(new Date('September 15, 2010 15:30:00'));
});
it('moves to the previous month & renders correctly when `previous` button is clicked', function() {
clickPreviousButton();
expect(getTitle()).toBe('August 2010');
expect(getLabels(true)).toEqual(['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']);
expect(getOptions(true)).toEqual([
['01', '02', '03', '04', '05', '06', '07'],
['08', '09', '10', '11', '12', '13', '14'],
['15', '16', '17', '18', '19', '20', '21'],
['22', '23', '24', '25', '26', '27', '28'],
['29', '30', '31', '01', '02', '03', '04'],
['05', '06', '07', '08', '09', '10', '11']
]);
expectSelectedElement(null, null);
});
it('updates the model only when a day is clicked in the `previous` month', function() {
clickPreviousButton();
expect($rootScope.date).toEqual(new Date('September 30, 2010 15:30:00'));
clickOption(17);
expect($rootScope.date).toEqual(new Date('August 18, 2010 15:30:00'));
});
it('moves to the next month & renders correctly when `next` button is clicked', function() {
clickNextButton();
expect(getTitle()).toBe('October 2010');
expect(getLabels(true)).toEqual(['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']);
expect(getOptions(true)).toEqual([
['26', '27', '28', '29', '30', '01', '02'],
['03', '04', '05', '06', '07', '08', '09'],
['10', '11', '12', '13', '14', '15', '16'],
['17', '18', '19', '20', '21', '22', '23'],
['24', '25', '26', '27', '28', '29', '30'],
['31', '01', '02', '03', '04', '05', '06']
]);
expectSelectedElement(4);
});
it('updates the model only when a day is clicked in the `next` month', function() {
clickNextButton();
expect($rootScope.date).toEqual(new Date('September 30, 2010 15:30:00'));
clickOption(17);
expect($rootScope.date).toEqual(new Date('October 13, 2010 15:30:00'));
});
it('updates the calendar when a day of another month is selected', function() {
clickOption(33);
expect($rootScope.date).toEqual(new Date('October 01, 2010 15:30:00'));
expect(getTitle()).toBe('October 2010');
expect(getLabels(true)).toEqual(['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']);
expect(getOptions(true)).toEqual([
['26', '27', '28', '29', '30', '01', '02'],
['03', '04', '05', '06', '07', '08', '09'],
['10', '11', '12', '13', '14', '15', '16'],
['17', '18', '19', '20', '21', '22', '23'],
['24', '25', '26', '27', '28', '29', '30'],
['31', '01', '02', '03', '04', '05', '06']
]);
expectSelectedElement(5);
});
// issue #1697
it('should not "jump" months', function() {
$rootScope.date = new Date('January 30, 2014');
$rootScope.$digest();
clickNextButton();
expect(getTitle()).toBe('February 2014');
clickPreviousButton();
expect(getTitle()).toBe('January 2014');
});
it('should not change model when going to next month - #5441', function() {
$rootScope.date = new Date('January 30, 2014');
$rootScope.$digest();
clickNextButton();
expect($rootScope.date).toEqual(new Date('January 30, 2014'));
});
describe('when `model` changes', function() {
function testCalendar() {
expect(getTitle()).toBe('November 2005');
expect(getOptions(true)).toEqual([
['30', '31', '01', '02', '03', '04', '05'],
['06', '07', '08', '09', '10', '11', '12'],
['13', '14', '15', '16', '17', '18', '19'],
['20', '21', '22', '23', '24', '25', '26'],
['27', '28', '29', '30', '01', '02', '03'],
['04', '05', '06', '07', '08', '09', '10']
]);
expectSelectedElement(8);
}
describe('to a Date object', function() {
it('updates', function() {
$rootScope.date = new Date('November 7, 2005 23:30:00');
$rootScope.$digest();
testCalendar();
expect(angular.isDate($rootScope.date)).toBe(true);
});
it('to a date that is invalid, it doesn\`t update', function() {
$rootScope.date = new Date('pizza');
$rootScope.$digest();
expect(getTitle()).toBe('September 2010');
expect(angular.isDate($rootScope.date)).toBe(true);
expect(isNaN($rootScope.date)).toBe(true);
});
});
describe('not to a Date object', function() {
it('to a Number, it updates calendar', function() {
$rootScope.date = parseInt((new Date('November 7, 2005 23:30:00')).getTime(), 10);
$rootScope.$digest();
testCalendar();
expect(angular.isNumber($rootScope.date)).toBe(true);
});
it('to a string that can be parsed by Date, it updates calendar', function() {
$rootScope.date = 'November 7, 2005 23:30:00';
$rootScope.$digest();
testCalendar();
expect(angular.isString($rootScope.date)).toBe(true);
});
it('to a string that cannot be parsed by Date, it doesn\'t update', function() {
$rootScope.date = 'pizza';
$rootScope.$digest();
expect(getTitle()).toBe('September 2010');
expect($rootScope.date).toBe('pizza');
});
});
});
it('does not loop between after max mode', function() {
expect(getTitle()).toBe('September 2010');
clickTitleButton();
expect(getTitle()).toBe('2010');
clickTitleButton();
expect(getTitle()).toBe('2001 - 2020');
clickTitleButton();
expect(getTitle()).toBe('2001 - 2020');
});
describe('month selection mode', function() {
beforeEach(function() {
clickTitleButton();
});
it('shows the year as title', function() {
expect(getTitle()).toBe('2010');
});
it('shows months as options', function() {
expect(getOptions()).toEqual([
['January', 'February', 'March'],
['April', 'May', 'June'],
['July', 'August', 'September'],
['October', 'November', 'December']
]);
});
it('does not change the model', function() {
expect($rootScope.date).toEqual(new Date('September 30, 2010 15:30:00'));
});
it('has `selected` only the correct month', function() {
expectSelectedElement(8);
});
it('moves to the previous year when `previous` button is clicked', function() {
clickPreviousButton();
expect(getTitle()).toBe('2009');
expect(getOptions()).toEqual([
['January', 'February', 'March'],
['April', 'May', 'June'],
['July', 'August', 'September'],
['October', 'November', 'December']
]);
expectSelectedElement(null);
});
it('moves to the next year when `next` button is clicked', function() {
clickNextButton();
expect(getTitle()).toBe('2011');
expect(getOptions()).toEqual([
['January', 'February', 'March'],
['April', 'May', 'June'],
['July', 'August', 'September'],
['October', 'November', 'December']
]);
expectSelectedElement(null);
});
it('renders correctly when a month is clicked', function() {
clickPreviousButton(5);
expect(getTitle()).toBe('2005');
clickOption(10);
expect($rootScope.date).toEqual(new Date('September 30, 2010 15:30:00'));
expect(getTitle()).toBe('November 2005');
expect(getOptions(true)).toEqual([
['30', '31', '01', '02', '03', '04', '05'],
['06', '07', '08', '09', '10', '11', '12'],
['13', '14', '15', '16', '17', '18', '19'],
['20', '21', '22', '23', '24', '25', '26'],
['27', '28', '29', '30', '01', '02', '03'],
['04', '05', '06', '07', '08', '09', '10']
]);
clickOption(17);
expect($rootScope.date).toEqual(new Date('November 16, 2005 15:30:00'));
});
});
describe('year selection mode', function() {
beforeEach(function() {
clickTitleButton();
clickTitleButton();
});
it('shows the year range as title', function() {
expect(getTitle()).toBe('2001 - 2020');
});
it('shows years as options', function() {
expect(getOptions()).toEqual([
['2001', '2002', '2003', '2004', '2005'],
['2006', '2007', '2008', '2009', '2010'],
['2011', '2012', '2013', '2014', '2015'],
['2016', '2017', '2018', '2019', '2020']
]);
});
it('does not change the model', function() {
expect($rootScope.date).toEqual(new Date('September 30, 2010 15:30:00'));
});
it('has `selected` only the selected year', function() {
expectSelectedElement(9);
});
it('moves to the previous year set when `previous` button is clicked', function() {
clickPreviousButton();
expect(getTitle()).toBe('1981 - 2000');
expect(getOptions()).toEqual([
['1981', '1982', '1983', '1984', '1985'],
['1986', '1987', '1988', '1989', '1990'],
['1991', '1992', '1993', '1994', '1995'],
['1996', '1997', '1998', '1999', '2000']
]);
expectSelectedElement(null);
});
it('moves to the next year set when `next` button is clicked', function() {
clickNextButton();
expect(getTitle()).toBe('2021 - 2040');
expect(getOptions()).toEqual([
['2021', '2022', '2023', '2024', '2025'],
['2026', '2027', '2028', '2029', '2030'],
['2031', '2032', '2033', '2034', '2035'],
['2036', '2037', '2038', '2039', '2040']
]);
expectSelectedElement(null);
});
});
describe('keyboard navigation', function() {
function getActiveLabel() {
return element.find('.active').eq(0).text();
}
describe('day mode', function() {
it('will be able to activate previous day', function() {
triggerKeyDown(element, 'left');
expect(getActiveLabel()).toBe('29');
});
it('will be able to select with enter', function() {
triggerKeyDown(element, 'left');
triggerKeyDown(element, 'enter');
expect($rootScope.date).toEqual(new Date('September 29, 2010 15:30:00'));
});
it('will be able to select with space', function() {
triggerKeyDown(element, 'left');
triggerKeyDown(element, 'space');
expect($rootScope.date).toEqual(new Date('September 29, 2010 15:30:00'));
});
it('will be able to activate next day', function() {
triggerKeyDown(element, 'right');
expect(getActiveLabel()).toBe('01');
expect(getTitle()).toBe('October 2010');
});
it('will be able to activate same day in previous week', function() {
triggerKeyDown(element, 'up');
expect(getActiveLabel()).toBe('23');
});
it('will be able to activate same day in next week', function() {
triggerKeyDown(element, 'down');
expect(getActiveLabel()).toBe('07');
expect(getTitle()).toBe('October 2010');
});
it('will be able to activate same date in previous month', function() {
triggerKeyDown(element, 'pageup');
expect(getActiveLabel()).toBe('30');
expect(getTitle()).toBe('August 2010');
});
it('will be able to activate same date in next month', function() {
triggerKeyDown(element, 'pagedown');
expect(getActiveLabel()).toBe('30');
expect(getTitle()).toBe('October 2010');
});
it('will be able to activate first day of the month', function() {
triggerKeyDown(element, 'home');
expect(getActiveLabel()).toBe('01');
expect(getTitle()).toBe('September 2010');
});
it('will be able to activate last day of the month', function() {
$rootScope.date = new Date('September 1, 2010 15:30:00');
$rootScope.$digest();
triggerKeyDown(element, 'end');
expect(getActiveLabel()).toBe('30');
expect(getTitle()).toBe('September 2010');
});
it('will be able to move to month mode', function() {
triggerKeyDown(element, 'up', true);
expect(getActiveLabel()).toBe('September');
expect(getTitle()).toBe('2010');
});
it('will not respond when trying to move to lower mode', function() {
triggerKeyDown(element, 'down', true);
expect(getActiveLabel()).toBe('30');
expect(getTitle()).toBe('September 2010');
});
});
describe('month mode', function() {
beforeEach(function() {
triggerKeyDown(element, 'up', true);
});
it('will be able to activate previous month', function() {
triggerKeyDown(element, 'left');
expect(getActiveLabel()).toBe('August');
});
it('will be able to activate next month', function() {
triggerKeyDown(element, 'right');
expect(getActiveLabel()).toBe('October');
});
it('will be able to activate same month in previous row', function() {
triggerKeyDown(element, 'up');
expect(getActiveLabel()).toBe('June');
});
it('will be able to activate same month in next row', function() {
triggerKeyDown(element, 'down');
expect(getActiveLabel()).toBe('December');
});
it('will be able to activate same date in previous year', function() {
triggerKeyDown(element, 'pageup');
expect(getActiveLabel()).toBe('September');
expect(getTitle()).toBe('2009');
});
it('will be able to activate same date in next year', function() {
triggerKeyDown(element, 'pagedown');
expect(getActiveLabel()).toBe('September');
expect(getTitle()).toBe('2011');
});
it('will be able to activate first month of the year', function() {
triggerKeyDown(element, 'home');
expect(getActiveLabel()).toBe('January');
expect(getTitle()).toBe('2010');
});
it('will be able to activate last month of the year', function() {
triggerKeyDown(element, 'end');
expect(getActiveLabel()).toBe('December');
expect(getTitle()).toBe('2010');
});
it('will be able to move to year mode', function() {
triggerKeyDown(element, 'up', true);
expect(getActiveLabel()).toBe('2010');
expect(getTitle()).toBe('2001 - 2020');
});
it('will be able to move to day mode', function() {
triggerKeyDown(element, 'down', true);
expect(getActiveLabel()).toBe('30');
expect(getTitle()).toBe('September 2010');
});
it('will move to day mode when selecting', function() {
triggerKeyDown(element, 'left', true);
triggerKeyDown(element, 'enter', true);
expect(getActiveLabel()).toBe('30');
expect(getTitle()).toBe('August 2010');
expect($rootScope.date).toEqual(new Date('September 30, 2010 15:30:00'));
});
});
describe('year mode', function() {
beforeEach(function() {
triggerKeyDown(element, 'up', true);
triggerKeyDown(element, 'up', true);
});
it('will be able to activate previous year', function() {
triggerKeyDown(element, 'left');
expect(getActiveLabel()).toBe('2009');
});
it('will be able to activate next year', function() {
triggerKeyDown(element, 'right');
expect(getActiveLabel()).toBe('2011');
});
it('will be able to activate same year in previous row', function() {
triggerKeyDown(element, 'up');
expect(getActiveLabel()).toBe('2005');
});
it('will be able to activate same year in next row', function() {
triggerKeyDown(element, 'down');
expect(getActiveLabel()).toBe('2015');
});
it('will be able to activate same date in previous view', function() {
triggerKeyDown(element, 'pageup');
expect(getActiveLabel()).toBe('1990');
});
it('will be able to activate same date in next view', function() {
triggerKeyDown(element, 'pagedown');
expect(getActiveLabel()).toBe('2030');
});
it('will be able to activate first year of the year', function() {
triggerKeyDown(element, 'home');
expect(getActiveLabel()).toBe('2001');
});
it('will be able to activate last year of the year', function() {
triggerKeyDown(element, 'end');
expect(getActiveLabel()).toBe('2020');
});
it('will not respond when trying to move to upper mode', function() {
triggerKeyDown(element, 'up', true);
expect(getTitle()).toBe('2001 - 2020');
});
it('will be able to move to month mode', function() {
triggerKeyDown(element, 'down', true);
expect(getActiveLabel()).toBe('September');
expect(getTitle()).toBe('2010');
});
it('will move to month mode when selecting', function() {
triggerKeyDown(element, 'left', true);
triggerKeyDown(element, 'enter', true);
expect(getActiveLabel()).toBe('September');
expect(getTitle()).toBe('2009');
expect($rootScope.date).toEqual(new Date('September 30, 2010 15:30:00'));
});
});
describe('`aria-activedescendant`', function() {
function checkActivedescendant() {
var activeId = element.find('table').attr('aria-activedescendant');
expect(element.find('#' + activeId + ' > button')).toHaveClass('active');
}
it('updates correctly', function() {
triggerKeyDown(element, 'left');
checkActivedescendant();
triggerKeyDown(element, 'down');
checkActivedescendant();
triggerKeyDown(element, 'up', true);
checkActivedescendant();
triggerKeyDown(element, 'up', true);
checkActivedescendant();
});
});
});
});
describe('attribute `datepicker-options`', function() {
describe('ngModelOptions', function() {
beforeEach(inject(function() {
$rootScope.date = new Date('2005-11-07T10:00:00.000Z');
$rootScope.options = {
ngModelOptions: {
timezone: '+600'
}
};
element = $compile('')($rootScope);
$rootScope.$digest();
}));
it('supports ngModelOptions from options object and sets date to appropriate date', function() {
expectSelectedElement(8);
});
});
describe('startingDay', function() {
beforeEach(function() {
$rootScope.options = {
startingDay: 1
};
element = $compile('')($rootScope);
$rootScope.$digest();
});
it('shows the day labels rotated', function() {
expect(getLabels(true)).toEqual(['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']);
});
it('renders the calendar days correctly', function() {
expect(getOptions(true)).toEqual([
['30', '31', '01', '02', '03', '04', '05'],
['06', '07', '08', '09', '10', '11', '12'],
['13', '14', '15', '16', '17', '18', '19'],
['20', '21', '22', '23', '24', '25', '26'],
['27', '28', '29', '30', '01', '02', '03'],
['04', '05', '06', '07', '08', '09', '10']
]);
});
it('renders the week numbers correctly', function() {
expect(getWeeks()).toEqual(['35', '36', '37', '38', '39', '40']);
});
});
describe('showWeeks', function() {
beforeEach(function() {
$rootScope.options = {
showWeeks: false
};
element = $compile('')($rootScope);
$rootScope.$digest();
});
it('hides week numbers based on variable', function() {
expect(getLabelsRow().find('th').length).toEqual(7);
var tr = element.find('tbody').find('tr');
for (var i = 0; i < 5; i++) {
expect(tr.eq(i).find('td').length).toEqual(7);
}
});
});
describe('minDate with no initial value', function() {
beforeEach(function() {
$rootScope.options = {};
$rootScope.date = new Date('September 10, 2010');
element = $compile('')($rootScope);
$rootScope.$digest();
});
it('should toggle appropriately', function() {
var buttons = getAllOptionsEl();
angular.forEach(buttons, function(button, index) {
expect(angular.element(button).prop('disabled')).toBe(false);
});
$rootScope.options.minDate = new Date('September 12, 2010');
$rootScope.$digest();
refreshedButtons = getAllOptionsEl();
angular.forEach(refreshedButtons, function(button, index) {
expect(angular.element(button).prop('disabled')).toBe(index < 14);
});
});
});
describe('minDate', function() {
beforeEach(function() {
$rootScope.options = {
minDate: new Date('September 12, 2010')
};
element = $compile('')($rootScope);
$rootScope.$digest();
});
it('disables appropriate days in current month', function() {
var buttons = getAllOptionsEl();
angular.forEach(buttons, function(button, index) {
expect(angular.element(button).prop('disabled')).toBe(index < 14);
});
});
it('disables appropriate days when min date changes', function() {
$rootScope.options.minDate = new Date('September 5, 2010');
$rootScope.$digest();
var buttons = getAllOptionsEl();
angular.forEach(buttons, function(button, index) {
expect(angular.element(button).prop('disabled')).toBe(index < 7);
});
});
it('invalidates when model is a disabled date', function() {
$rootScope.options.minDate = new Date('September 5, 2010');
$rootScope.date = new Date('September 2, 2010');
$rootScope.$digest();
expect(element.hasClass('ng-invalid')).toBeTruthy();
expect(element.hasClass('ng-invalid-date-disabled')).toBeTruthy();
});
it('disables all days in previous month', function() {
clickPreviousButton();
var buttons = getAllOptionsEl();
angular.forEach(buttons, function(button, index) {
expect(angular.element(button).prop('disabled')).toBe(true);
});
});
it('disables no days in next month', function() {
clickNextButton();
var buttons = getAllOptionsEl();
angular.forEach(buttons, function(button, index) {
expect(angular.element(button).prop('disabled')).toBe(false);
});
});
it('disables appropriate months in current year', function() {
clickTitleButton();
var buttons = getAllOptionsEl();
angular.forEach(buttons, function(button, index) {
expect(angular.element(button).prop('disabled')).toBe(index < 8);
});
});
it('disables all months in previous year', function() {
clickTitleButton();
clickPreviousButton();
var buttons = getAllOptionsEl();
angular.forEach(buttons, function(button, index) {
expect(angular.element(button).prop('disabled')).toBe(true);
});
});
it('disables no months in next year', function() {
clickTitleButton();
clickNextButton();
var buttons = getAllOptionsEl();
angular.forEach(buttons, function(button, index) {
expect(angular.element(button).prop('disabled')).toBe(false);
});
});
it('enables everything before if it is cleared', function() {
$rootScope.options.minDate = null;
$rootScope.date = new Date('December 20, 1949');
$rootScope.$digest();
clickTitleButton();
var buttons = getAllOptionsEl();
angular.forEach(buttons, function(button, index) {
expect(angular.element(button).prop('disabled')).toBe(false);
});
});
it('accepts literals, \'yyyy-MM-dd\' case', function() {
$rootScope.options.minDate = '2010-09-05';
element = $compile('')($rootScope);
$rootScope.$digest();
var buttons = getAllOptionsEl();
angular.forEach(buttons, function(button, index) {
expect(angular.element(button).prop('disabled')).toBe(index < 7);
});
});
});
describe('maxDate with no initial value', function() {
beforeEach(function() {
$rootScope.options = {};
$rootScope.date = new Date('September 10, 2010');
element = $compile('')($rootScope);
$rootScope.$digest();
});
it('should toggle appropriately', function() {
var buttons = getAllOptionsEl();
angular.forEach(buttons, function(button, index) {
expect(angular.element(button).prop('disabled')).toBe(false);
});
$rootScope.options.maxDate = new Date('September 25, 2010');
$rootScope.$digest();
refreshedButtons = getAllOptionsEl();
angular.forEach(refreshedButtons, function(button, index) {
expect(angular.element(button).prop('disabled')).toBe(index > 27);
});
});
});
describe('maxDate', function() {
beforeEach(function() {
$rootScope.options = {
maxDate: new Date('September 25, 2010')
};
element = $compile('')($rootScope);
$rootScope.$digest();
});
it('disables appropriate days in current month', function() {
var buttons = getAllOptionsEl();
angular.forEach(buttons, function(button, index) {
expect(angular.element(button).prop('disabled')).toBe(index > 27);
});
});
it('disables appropriate days when max date changes', function() {
$rootScope.options.maxDate = new Date('September 18, 2010');
$rootScope.$digest();
var buttons = getAllOptionsEl();
angular.forEach(buttons, function(button, index) {
expect(angular.element(button).prop('disabled')).toBe(index > 20);
});
});
it('invalidates when model is a disabled date', function() {
$rootScope.options.maxDate = new Date('September 18, 2010');
$rootScope.$digest();
expect(element.hasClass('ng-invalid')).toBeTruthy();
expect(element.hasClass('ng-invalid-date-disabled')).toBeTruthy();
});
it('disables no days in previous month', function() {
clickPreviousButton();
var buttons = getAllOptionsEl();
angular.forEach(buttons, function(button, index) {
expect(angular.element(button).prop('disabled')).toBe(false);
});
});
it('disables all days in next month', function() {
clickNextButton();
var buttons = getAllOptionsEl();
angular.forEach(buttons, function(button, index) {
expect(angular.element(button).prop('disabled')).toBe(true);
});
});
it('disables appropriate months in current year', function() {
clickTitleButton();
var buttons = getAllOptionsEl();
angular.forEach(buttons, function(button, index) {
expect(angular.element(button).prop('disabled')).toBe(index > 8);
});
});
it('disables no months in previous year', function() {
clickTitleButton();
clickPreviousButton();
var buttons = getAllOptionsEl();
angular.forEach(buttons, function(button, index) {
expect(angular.element(button).prop('disabled')).toBe(false);
});
});
it('disables all months in next year', function() {
clickTitleButton();
clickNextButton();
var buttons = getAllOptionsEl();
angular.forEach(buttons, function(button, index) {
expect(angular.element(button).prop('disabled')).toBe(true);
});
});
it('enables everything after if it is cleared', function() {
$rootScope.options.maxDate = null;
$rootScope.$digest();
var buttons = getAllOptionsEl();
angular.forEach(buttons, function(button, index) {
expect(angular.element(button).prop('disabled')).toBe(false);
});
});
});
describe('formatting', function() {
beforeEach(function() {
$rootScope.options = {
formatDay: 'd',
formatDayHeader: 'EEEE',
formatDayTitle: 'MMMM, yy',
formatMonth: 'MMM',
formatMonthTitle: 'yy',
formatYear: 'yy',
yearColumns: 4,
yearRows: 3
};
element = $compile('')($rootScope);
$rootScope.$digest();
});
it('changes the title format in `day` mode', function() {
expect(getTitle()).toBe('September, 10');
});
it('changes the title & months format in `month` mode', function() {
clickTitleButton();
expect(getTitle()).toBe('10');
expect(getOptions()).toEqual([
['Jan', 'Feb', 'Mar'],
['Apr', 'May', 'Jun'],
['Jul', 'Aug', 'Sep'],
['Oct', 'Nov', 'Dec']
]);
});
it('changes the title, year format & range in `year` mode', function() {
clickTitleButton();
clickTitleButton();
expect(getTitle()).toBe('05 - 16');
expect(getOptions()).toEqual([
['05', '06', '07', '08'],
['09', '10', '11', '12'],
['13', '14', '15', '16']
]);
});
it('shows day labels', function() {
expect(getLabels(true)).toEqual(['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']);
});
it('changes the day format', function() {
expect(getOptions(true)).toEqual([
['29', '30', '31', '1', '2', '3', '4'],
['5', '6', '7', '8', '9', '10', '11'],
['12', '13', '14', '15', '16', '17', '18'],
['19', '20', '21', '22', '23', '24', '25'],
['26', '27', '28', '29', '30', '1', '2'],
['3', '4', '5', '6', '7', '8', '9']
]);
});
});
});
describe('setting datepickerConfig', function() {
var originalConfig = {};
beforeEach(inject(function(uibDatepickerConfig) {
angular.extend(originalConfig, uibDatepickerConfig);
uibDatepickerConfig.formatDay = 'd';
uibDatepickerConfig.formatMonth = 'MMM';
uibDatepickerConfig.formatYear = 'yy';
uibDatepickerConfig.formatDayHeader = 'EEEE';
uibDatepickerConfig.formatDayTitle = 'MMM, yy';
uibDatepickerConfig.formatMonthTitle = 'yy';
uibDatepickerConfig.showWeeks = false;
uibDatepickerConfig.yearRows = 2;
uibDatepickerConfig.yearColumns = 5;
uibDatepickerConfig.startingDay = 6;
uibDatepickerConfig.monthColumns = 4;
element = $compile('')($rootScope);
$rootScope.$digest();
}));
afterEach(inject(function(uibDatepickerConfig) {
// return it to the original state
Object.keys(uibDatepickerConfig).forEach(function(key) {
delete uibDatepickerConfig[key];
});
angular.extend(uibDatepickerConfig, originalConfig);
}));
it('changes the title format in `day` mode', function() {
expect(getTitle()).toBe('Sep, 10');
});
it('changes the title & months format in `month` mode', function() {
clickTitleButton();
expect(getTitle()).toBe('10');
expect(getOptions()).toEqual([
['Jan', 'Feb', 'Mar', 'Apr'],
['May', 'Jun', 'Jul', 'Aug'],
['Sep', 'Oct', 'Nov', 'Dec']
]);
});
it('shows title year button to expand to fill width in `month` mode', function() {
clickTitleButton();
expect(getTitleCell().attr('colspan')).toBe('2');
});
it('changes the title, year format & range in `year` mode', function() {
clickTitleButton();
clickTitleButton();
expect(getTitle()).toBe('01 - 10');
expect(getOptions()).toEqual([
['01', '02', '03', '04', '05'],
['06', '07', '08', '09', '10']
]);
});
it('changes the `starting-day` & day headers & format', function() {
expect(getLabels()).toEqual(['Saturday', 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday']);
expect(getOptions(false)).toEqual([
['28', '29', '30', '31', '1', '2', '3'],
['4', '5', '6', '7', '8', '9', '10'],
['11', '12', '13', '14', '15', '16', '17'],
['18', '19', '20', '21', '22', '23', '24'],
['25', '26', '27', '28', '29', '30', '1'],
['2', '3', '4', '5', '6', '7', '8']
]);
});
it('changes initial visibility for weeks', function() {
expect(getLabelsRow().find('th').length).toEqual(7);
var tr = element.find('tbody').find('tr');
for (var i = 0; i < 5; i++) {
expect(tr.eq(i).find('td').length).toEqual(7);
}
});
});
describe('disabled', function() {
beforeEach(function() {
element = $compile('')($rootScope);
$rootScope.$digest();
});
it('should have all dates disabled', function() {
element.find('.uib-day button').each(function(idx, elem) {
expect($(elem).prop('disabled')).toBe(true);
});
});
});
describe('ng-disabled', function() {
beforeEach(function() {
$rootScope.disabled = false;
element = $compile('')($rootScope);
$rootScope.$digest();
});
it('should toggle disabled state with value of ng-disabled', function() {
element.find('.uib-day button').each(function(idx, elem) {
expect($(elem).prop('disabled')).toBe(false);
});
$rootScope.disabled = true;
$rootScope.$digest();
element.find('.uib-day button').each(function(idx, elem) {
expect($(elem).prop('disabled')).toBe(true);
});
$rootScope.disabled = false;
$rootScope.$digest();
element.find('.uib-day button').each(function(idx, elem) {
expect($(elem).prop('disabled')).toBe(false);
});
});
});
describe('datepickerConfig ngModelOptions', function() {
describe('timezone', function() {
var originalConfig = {};
beforeEach(inject(function(uibDatepickerConfig) {
angular.extend(originalConfig, uibDatepickerConfig);
uibDatepickerConfig.ngModelOptions = { timezone: '+600' };
$rootScope.date = new Date('2005-11-07T10:00:00.000Z');
}));
afterEach(inject(function(uibDatepickerConfig) {
// return it to the original state
angular.extend(uibDatepickerConfig, originalConfig);
}));
describe('basics', function() {
beforeEach(function() {
element = $compile('')($rootScope);
$rootScope.$digest();
});
it('sets date to appropriate date', function() {
expectSelectedElement(8);
});
it('updates the input when a day is clicked', function() {
clickOption(9);
expect($rootScope.date).toEqual(new Date('2005-11-08T10:00:00.000Z'));
});
});
it('init date', function() {
$rootScope.options = {
initDate: new Date('2006-01-01T00:00:00.000Z')
};
$rootScope.date = null;
element = $compile('')($rootScope);
$rootScope.$digest();
expect(getTitle()).toEqual('January 2006');
});
it('min date', function() {
$rootScope.options = {
minDate: new Date('2010-10-01T00:00:00.000Z')
};
element = $compile('')($rootScope);
$rootScope.$digest();
expect(getSelectedElement().prop('disabled')).toBe(true);
});
});
});
describe('uib-datepicker ng-model-options', function() {
describe('timezone', function() {
beforeEach(inject(function() {
$rootScope.date = new Date('2005-11-07T10:00:00.000Z');
$rootScope.ngModelOptions = { timezone: '+600'};
element = $compile('')($rootScope);
$rootScope.$digest();
}));
it('sets date to appropriate date', function() {
expectSelectedElement(8);
});
it('updates the input when a day is clicked', function() {
clickOption(9);
expect($rootScope.date).toEqual(new Date('2005-11-08T10:00:00.000Z'));
});
});
});
describe('with empty initial state', function() {
beforeEach(inject(function() {
$rootScope.date = null;
element = $compile('')($rootScope);
$rootScope.$digest();
}));
it('is has a `` element', function() {
expect(element.find('table').length).toBe(1);
});
it('is shows rows with days', function() {
expect(element.find('tbody').find('tr').length).toBeGreaterThan(3);
});
it('sets default 00:00:00 time for selected date', function() {
$rootScope.date = new Date('August 1, 2013');
$rootScope.$digest();
$rootScope.date = null;
$rootScope.$digest();
clickOption(14);
expect($rootScope.date).toEqual(new Date('August 11, 2013 00:00:00'));
});
});
describe('`init-date`', function() {
beforeEach(inject(function() {
$rootScope.date = null;
$rootScope.options = {
initDate: new Date('November 9, 1980')
};
element = $compile('')($rootScope);
$rootScope.$digest();
}));
it('does not alter the model', function() {
expect($rootScope.date).toBe(null);
});
it('shows the correct title', function() {
expect(getTitle()).toBe('November 1980');
});
});
describe('`datepicker-mode`', function() {
beforeEach(inject(function() {
$rootScope.date = new Date('August 11, 2013');
$rootScope.options = {
datepickerMode: 'month'
};
element = $compile('')($rootScope);
$rootScope.$digest();
}));
it('shows the correct title', function() {
expect(getTitle()).toBe('2013');
});
it('updates binding', function() {
clickTitleButton();
expect($rootScope.options.datepickerMode).toBe('year');
});
});
describe('`min-mode`', function() {
beforeEach(inject(function() {
$rootScope.date = new Date('August 11, 2013');
$rootScope.options = {
minMode: 'month',
datepickerMode: 'month'
};
element = $compile('')($rootScope);
$rootScope.$digest();
}));
it('does not move below it', function() {
expect(getTitle()).toBe('2013');
clickOption( 5 );
expect(getTitle()).toBe('2013');
clickTitleButton();
expect(getTitle()).toBe('2001 - 2020');
$rootScope.options.minMode = 'year';
$rootScope.$digest();
clickOption( 5 );
expect(getTitle()).toBe('2001 - 2020');
});
it('updates current mode if necessary', function() {
expect(getTitle()).toBe('2013');
$rootScope.options.minMode = 'year';
$rootScope.$digest();
expect(getTitle()).toBe('2001 - 2020');
});
});
describe('`max-mode`', function() {
beforeEach(inject(function() {
$rootScope.date = new Date('August 11, 2013');
$rootScope.options = {
maxMode: 'month'
};
element = $compile('')($rootScope);
$rootScope.$digest();
}));
it('does not move above it', function() {
expect(getTitle()).toBe('August 2013');
clickTitleButton();
expect(getTitle()).toBe('2013');
clickTitleButton();
expect(getTitle()).toBe('2013');
clickOption( 10 );
expect(getTitle()).toBe('November 2013');
$rootScope.options.maxMode = 'day';
$rootScope.$digest();
clickTitleButton();
expect(getTitle()).toBe('November 2013');
});
it('disables the title button at it', function() {
expect(getTitleButton().prop('disabled')).toBe(false);
clickTitleButton();
expect(getTitleButton().prop('disabled')).toBe(true);
clickTitleButton();
expect(getTitleButton().prop('disabled')).toBe(true);
clickOption( 10 );
expect(getTitleButton().prop('disabled')).toBe(false);
$rootScope.options.maxMode = 'day';
$rootScope.$digest();
expect(getTitleButton().prop('disabled')).toBe(true);
});
it('updates current mode if necessary', function() {
expect(getTitle()).toBe('August 2013');
clickTitleButton();
expect(getTitle()).toBe('2013');
$rootScope.options.maxMode = 'day';
$rootScope.$digest();
expect(getTitle()).toBe('August 2013');
});
});
describe('with an ngModelController having formatters and parsers', function() {
beforeEach(inject(function() {
// Custom date object.
$rootScope.date = { type: 'date', date: 'April 1, 2015 00:00:00' };
// Use dateModel directive to add formatters and parsers to the
// ngModelController that translate the custom date object.
element = $compile('')($rootScope);
$rootScope.$digest();
}));
it('updates the view', function() {
$rootScope.date = { type: 'date', date: 'April 15, 2015 00:00:00' };
$rootScope.$digest();
expectSelectedElement(17);
});
it('updates the model', function() {
clickOption(17);
expect($rootScope.date.type).toEqual('date');
expect(new Date($rootScope.date.date)).toEqual(new Date('April 15, 2015 00:00:00'));
});
});
describe('thursdays determine week count', function() {
beforeEach(inject(function() {
$rootScope.date = new Date('June 07, 2014');
}));
it('with the default starting day (sunday)', function() {
element = $compile('')($rootScope);
$rootScope.$digest();
expect(getWeeks()).toEqual(['23', '24', '25', '26', '27', '28']);
});
describe('when starting date', function() {
it('is monday', function() {
$rootScope.options = {
startingDay: 1
};
element = $compile('')($rootScope);
$rootScope.$digest();
expect(getWeeks()).toEqual(['22', '23', '24', '25', '26', '27']);
});
it('is thursday', function() {
$rootScope.options = {
startingDay: 4
};
element = $compile('')($rootScope);
$rootScope.$digest();
expect(getWeeks()).toEqual(['22', '23', '24', '25', '26', '27']);
});
it('is saturday', function() {
$rootScope.options = {
startingDay: 6
};
element = $compile('')($rootScope);
$rootScope.$digest();
expect(getWeeks()).toEqual(['23', '24', '25', '26', '27', '28']);
});
});
describe('first week in january', function() {
it('in current year', function() {
$rootScope.date = new Date('January 07, 2014');
element = $compile('')($rootScope);
$rootScope.$digest();
expect(getWeeks()).toEqual(['1', '2', '3', '4', '5', '6']);
});
it('in last year', function() {
$rootScope.date = new Date('January 07, 2010');
element = $compile('')($rootScope);
$rootScope.$digest();
expect(getWeeks()).toEqual(['53', '1', '2', '3', '4', '5']);
});
});
describe('last week(s) in december', function() {
beforeEach(inject(function() {
$rootScope.date = new Date('December 07, 2014');
}));
it('in next year', function() {
element = $compile('')($rootScope);
$rootScope.$digest();
expect(getWeeks()).toEqual(['49', '50', '51', '52', '1', '2']);
});
});
});
});
});
================================================
FILE: src/datepickerPopup/docs/demo.html
================================================
Selected date is: {{dt | date:'fullDate' }}
Popup
================================================
FILE: src/datepickerPopup/docs/demo.js
================================================
angular.module('ui.bootstrap.demo').controller('DatepickerPopupDemoCtrl', function ($scope) {
$scope.today = function() {
$scope.dt = new Date();
};
$scope.today();
$scope.clear = function() {
$scope.dt = null;
};
$scope.inlineOptions = {
customClass: getDayClass,
minDate: new Date(),
showWeeks: true
};
$scope.dateOptions = {
dateDisabled: disabled,
formatYear: 'yy',
maxDate: new Date(2020, 5, 22),
minDate: new Date(),
startingDay: 1
};
// Disable weekend selection
function disabled(data) {
var date = data.date,
mode = data.mode;
return mode === 'day' && (date.getDay() === 0 || date.getDay() === 6);
}
$scope.toggleMin = function() {
$scope.inlineOptions.minDate = $scope.inlineOptions.minDate ? null : new Date();
$scope.dateOptions.minDate = $scope.inlineOptions.minDate;
};
$scope.toggleMin();
$scope.open1 = function() {
$scope.popup1.opened = true;
};
$scope.open2 = function() {
$scope.popup2.opened = true;
};
$scope.setDate = function(year, month, day) {
$scope.dt = new Date(year, month, day);
};
$scope.formats = ['dd-MMMM-yyyy', 'yyyy/MM/dd', 'dd.MM.yyyy', 'shortDate'];
$scope.format = $scope.formats[0];
$scope.altInputFormats = ['M!/d!/yyyy'];
$scope.popup1 = {
opened: false
};
$scope.popup2 = {
opened: false
};
var tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
var afterTomorrow = new Date();
afterTomorrow.setDate(tomorrow.getDate() + 1);
$scope.events = [
{
date: tomorrow,
status: 'full'
},
{
date: afterTomorrow,
status: 'partially'
}
];
function getDayClass(data) {
var date = data.date,
mode = data.mode;
if (mode === 'day') {
var dayToCheck = new Date(date).setHours(0,0,0,0);
for (var i = 0; i < $scope.events.length; i++) {
var currentDay = new Date($scope.events[i].date).setHours(0,0,0,0);
if (dayToCheck === currentDay) {
return $scope.events[i].status;
}
}
}
return '';
}
});
================================================
FILE: src/datepickerPopup/docs/readme.md
================================================
The datepicker popup is meant to be used with an input element. To understand usage of the datepicker, please refer to its documentation [here](https://angular-ui.github.io/bootstrap/#/datepicker).
### uib-datepicker-popup settings
The popup is a wrapper that you can use in an input to toggle a datepicker. To configure the datepicker, use `datepicker-options` as documented in the [inline datepicker](https://angular-ui.github.io/bootstrap/#/datepicker).
* `alt-input-formats`
$
C
_(Default: `[]`)_ -
A list of alternate formats acceptable for manual entry.
* `clear-text`
C
_(Default: `Clear`)_ -
The text to display for the clear button.
* `close-on-date-selection`
$
C
_(Default: `true`)_ -
Whether to close calendar when a date is chosen.
* `close-text`
C
_(Default: `Done`)_ -
The text to display for the close button.
* `current-text`
C
_(Default: `Today`)_ -
The text to display for the current day button.
* `datepicker-append-to-body`
$
C
_(Default: `false`, Config: `appendToBody`)_ -
Append the datepicker popup element to `body`, rather than inserting after `datepicker-popup`.
* `datepicker-options`
$ -
An object with any combination of the datepicker settings (in camelCase) used to configure the wrapped datepicker.
* `datepicker-popup-template-url`
C
_(Default: `uib/template/datepickerPopup/popup.html`)_ -
Add the ability to override the template used on the component.
* `datepicker-template-url`
C
_(Default: `uib/template/datepicker/datepicker.html`)_ -
Add the ability to override the template used on the component (inner uib-datepicker).
* `is-open`
$
_(Default: `false`)_ -
Whether or not to show the datepicker.
* `ng-model`
$
-
The date object. Must be a Javascript `Date` object. You may use the `uibDateParser` service to assist in string-to-object conversion.
* `on-open-focus`
$
C
_(Default: `true`)_ -
Whether or not to focus the datepicker popup upon opening.
* `show-button-bar`
$
C
_(Default: `true`)_ -
Whether or not to display a button bar underneath the uib-datepicker.
* `type`
C
_(Default: `text`, Config: `html5Types`)_ -
You can override the input type to be _(date|datetime-local|month)_. That will change the date format of the popup.
* `popup-placement`
C
_(Default: `auto bottom-left`, Config: 'placement')_ -
Passing in 'auto' separated by a space before the placement will enable auto positioning, e.g: "auto bottom-left". The popup will attempt to position where it fits in the closest scrollable ancestor. Accepts:
* `top` - popup on top, horizontally centered on input element.
* `top-left` - popup on top, left edge aligned with input element left edge.
* `top-right` - popup on top, right edge aligned with input element right edge.
* `bottom` - popup on bottom, horizontally centered on input element.
* `bottom-left` - popup on bottom, left edge aligned with input element left edge.
* `bottom-right` - popup on bottom, right edge aligned with input element right edge.
* `left` - popup on left, vertically centered on input element.
* `left-top` - popup on left, top edge aligned with input element top edge.
* `left-bottom` - popup on left, bottom edge aligned with input element bottom edge.
* `right` - popup on right, vertically centered on input element.
* `right-top` - popup on right, top edge aligned with input element top edge.
* `right-bottom` - popup on right, bottom edge aligned with input element bottom edge.
* `uib-datepicker-popup`
C
_(Default: `yyyy-MM-dd`, Config: `datepickerConfig`)_ -
The format for displayed dates. This string can take string literals by surrounding the value with single quotes, i.e. `yyyy-MM-dd h 'o\'clock'`.
**Notes**
If using this directive on input type date, a native browser datepicker could also appear.
================================================
FILE: src/datepickerPopup/index-nocss.js
================================================
require('../datepicker/index-nocss.js');
require('../position/index-nocss.js');
require('../../template/datepickerPopup/popup.html.js');
require('./popup.js');
var MODULE_NAME = 'ui.bootstrap.module.datepickerPopup';
angular.module(MODULE_NAME, ['ui.bootstrap.datepickerPopup', 'uib/template/datepickerPopup/popup.html', 'ui.bootstrap.module.datepicker']);
module.exports = MODULE_NAME;
================================================
FILE: src/datepickerPopup/index.js
================================================
require('../datepicker/datepicker.css');
require('../position/position.css');
require('./popup.css');
module.exports = require('./index-nocss.js');
================================================
FILE: src/datepickerPopup/popup.css
================================================
.uib-datepicker-popup.dropdown-menu {
display: block;
float: none;
margin: 0;
}
.uib-button-bar {
padding: 10px 9px 2px;
}
================================================
FILE: src/datepickerPopup/popup.js
================================================
angular.module('ui.bootstrap.datepickerPopup', ['ui.bootstrap.datepicker', 'ui.bootstrap.position'])
.value('$datepickerPopupLiteralWarning', true)
.constant('uibDatepickerPopupConfig', {
altInputFormats: [],
appendToBody: false,
clearText: 'Clear',
closeOnDateSelection: true,
closeText: 'Done',
currentText: 'Today',
datepickerPopup: 'yyyy-MM-dd',
datepickerPopupTemplateUrl: 'uib/template/datepickerPopup/popup.html',
datepickerTemplateUrl: 'uib/template/datepicker/datepicker.html',
html5Types: {
date: 'yyyy-MM-dd',
'datetime-local': 'yyyy-MM-ddTHH:mm:ss.sss',
'month': 'yyyy-MM'
},
onOpenFocus: true,
showButtonBar: true,
placement: 'auto bottom-left'
})
.controller('UibDatepickerPopupController', ['$scope', '$element', '$attrs', '$compile', '$log', '$parse', '$window', '$document', '$rootScope', '$uibPosition', 'dateFilter', 'uibDateParser', 'uibDatepickerPopupConfig', '$timeout', 'uibDatepickerConfig', '$datepickerPopupLiteralWarning',
function($scope, $element, $attrs, $compile, $log, $parse, $window, $document, $rootScope, $position, dateFilter, dateParser, datepickerPopupConfig, $timeout, datepickerConfig, $datepickerPopupLiteralWarning) {
var cache = {},
isHtml5DateInput = false;
var dateFormat, closeOnDateSelection, appendToBody, onOpenFocus,
datepickerPopupTemplateUrl, datepickerTemplateUrl, popupEl, datepickerEl, scrollParentEl,
ngModel, ngModelOptions, $popup, altInputFormats, watchListeners = [];
this.init = function(_ngModel_) {
ngModel = _ngModel_;
ngModelOptions = extractOptions(ngModel);
closeOnDateSelection = angular.isDefined($attrs.closeOnDateSelection) ?
$scope.$parent.$eval($attrs.closeOnDateSelection) :
datepickerPopupConfig.closeOnDateSelection;
appendToBody = angular.isDefined($attrs.datepickerAppendToBody) ?
$scope.$parent.$eval($attrs.datepickerAppendToBody) :
datepickerPopupConfig.appendToBody;
onOpenFocus = angular.isDefined($attrs.onOpenFocus) ?
$scope.$parent.$eval($attrs.onOpenFocus) : datepickerPopupConfig.onOpenFocus;
datepickerPopupTemplateUrl = angular.isDefined($attrs.datepickerPopupTemplateUrl) ?
$attrs.datepickerPopupTemplateUrl :
datepickerPopupConfig.datepickerPopupTemplateUrl;
datepickerTemplateUrl = angular.isDefined($attrs.datepickerTemplateUrl) ?
$attrs.datepickerTemplateUrl : datepickerPopupConfig.datepickerTemplateUrl;
altInputFormats = angular.isDefined($attrs.altInputFormats) ?
$scope.$parent.$eval($attrs.altInputFormats) :
datepickerPopupConfig.altInputFormats;
$scope.showButtonBar = angular.isDefined($attrs.showButtonBar) ?
$scope.$parent.$eval($attrs.showButtonBar) :
datepickerPopupConfig.showButtonBar;
if (datepickerPopupConfig.html5Types[$attrs.type]) {
dateFormat = datepickerPopupConfig.html5Types[$attrs.type];
isHtml5DateInput = true;
} else {
dateFormat = $attrs.uibDatepickerPopup || datepickerPopupConfig.datepickerPopup;
$attrs.$observe('uibDatepickerPopup', function(value, oldValue) {
var newDateFormat = value || datepickerPopupConfig.datepickerPopup;
// Invalidate the $modelValue to ensure that formatters re-run
// FIXME: Refactor when PR is merged: https://github.com/angular/angular.js/pull/10764
if (newDateFormat !== dateFormat) {
dateFormat = newDateFormat;
ngModel.$modelValue = null;
if (!dateFormat) {
throw new Error('uibDatepickerPopup must have a date format specified.');
}
}
});
}
if (!dateFormat) {
throw new Error('uibDatepickerPopup must have a date format specified.');
}
if (isHtml5DateInput && $attrs.uibDatepickerPopup) {
throw new Error('HTML5 date input types do not support custom formats.');
}
// popup element used to display calendar
popupEl = angular.element('');
popupEl.attr({
'ng-model': 'date',
'ng-change': 'dateSelection(date)',
'template-url': datepickerPopupTemplateUrl
});
// datepicker element
datepickerEl = angular.element(popupEl.children()[0]);
datepickerEl.attr('template-url', datepickerTemplateUrl);
if (!$scope.datepickerOptions) {
$scope.datepickerOptions = {};
}
if (isHtml5DateInput) {
if ($attrs.type === 'month') {
$scope.datepickerOptions.datepickerMode = 'month';
$scope.datepickerOptions.minMode = 'month';
}
}
datepickerEl.attr('datepicker-options', 'datepickerOptions');
if (!isHtml5DateInput) {
// Internal API to maintain the correct ng-invalid-[key] class
ngModel.$$parserName = 'date';
ngModel.$validators.date = validator;
ngModel.$parsers.unshift(parseDate);
ngModel.$formatters.push(function(value) {
if (ngModel.$isEmpty(value)) {
$scope.date = value;
return value;
}
if (angular.isNumber(value)) {
value = new Date(value);
}
$scope.date = dateParser.fromTimezone(value, ngModelOptions.getOption('timezone'));
return dateParser.filter($scope.date, dateFormat);
});
} else {
ngModel.$formatters.push(function(value) {
$scope.date = dateParser.fromTimezone(value, ngModelOptions.getOption('timezone'));
return value;
});
}
// Detect changes in the view from the text box
ngModel.$viewChangeListeners.push(function() {
$scope.date = parseDateString(ngModel.$viewValue);
});
$element.on('keydown', inputKeydownBind);
$popup = $compile(popupEl)($scope);
// Prevent jQuery cache memory leak (template is now redundant after linking)
popupEl.remove();
if (appendToBody) {
$document.find('body').append($popup);
} else {
$element.after($popup);
}
$scope.$on('$destroy', function() {
if ($scope.isOpen === true) {
if (!$rootScope.$$phase) {
$scope.$apply(function() {
$scope.isOpen = false;
});
}
}
$popup.remove();
$element.off('keydown', inputKeydownBind);
$document.off('click', documentClickBind);
if (scrollParentEl) {
scrollParentEl.off('scroll', positionPopup);
}
angular.element($window).off('resize', positionPopup);
//Clear all watch listeners on destroy
while (watchListeners.length) {
watchListeners.shift()();
}
});
};
$scope.getText = function(key) {
return $scope[key + 'Text'] || datepickerPopupConfig[key + 'Text'];
};
$scope.isDisabled = function(date) {
if (date === 'today') {
date = dateParser.fromTimezone(new Date(), ngModelOptions.getOption('timezone'));
}
var dates = {};
angular.forEach(['minDate', 'maxDate'], function(key) {
if (!$scope.datepickerOptions[key]) {
dates[key] = null;
} else if (angular.isDate($scope.datepickerOptions[key])) {
dates[key] = new Date($scope.datepickerOptions[key]);
} else {
if ($datepickerPopupLiteralWarning) {
$log.warn('Literal date support has been deprecated, please switch to date object usage');
}
dates[key] = new Date(dateFilter($scope.datepickerOptions[key], 'medium'));
}
});
return $scope.datepickerOptions &&
dates.minDate && $scope.compare(date, dates.minDate) < 0 ||
dates.maxDate && $scope.compare(date, dates.maxDate) > 0;
};
$scope.compare = function(date1, date2) {
return new Date(date1.getFullYear(), date1.getMonth(), date1.getDate()) - new Date(date2.getFullYear(), date2.getMonth(), date2.getDate());
};
// Inner change
$scope.dateSelection = function(dt) {
$scope.date = dt;
var date = $scope.date ? dateParser.filter($scope.date, dateFormat) : null; // Setting to NULL is necessary for form validators to function
$element.val(date);
ngModel.$setViewValue(date);
if (closeOnDateSelection) {
$scope.isOpen = false;
$element[0].focus();
}
};
$scope.keydown = function(evt) {
if (evt.which === 27) {
evt.stopPropagation();
$scope.isOpen = false;
$element[0].focus();
}
};
$scope.select = function(date, evt) {
evt.stopPropagation();
if (date === 'today') {
var today = new Date();
if (angular.isDate($scope.date)) {
date = new Date($scope.date);
date.setFullYear(today.getFullYear(), today.getMonth(), today.getDate());
} else {
date = dateParser.fromTimezone(today, ngModelOptions.getOption('timezone'));
date.setHours(0, 0, 0, 0);
}
}
$scope.dateSelection(date);
};
$scope.close = function(evt) {
evt.stopPropagation();
$scope.isOpen = false;
$element[0].focus();
};
$scope.disabled = angular.isDefined($attrs.disabled) || false;
if ($attrs.ngDisabled) {
watchListeners.push($scope.$parent.$watch($parse($attrs.ngDisabled), function(disabled) {
$scope.disabled = disabled;
}));
}
$scope.$watch('isOpen', function(value) {
if (value) {
if (!$scope.disabled) {
$timeout(function() {
positionPopup();
if (onOpenFocus) {
$scope.$broadcast('uib:datepicker.focus');
}
$document.on('click', documentClickBind);
var placement = $attrs.popupPlacement ? $attrs.popupPlacement : datepickerPopupConfig.placement;
if (appendToBody || $position.parsePlacement(placement)[2]) {
scrollParentEl = scrollParentEl || angular.element($position.scrollParent($element));
if (scrollParentEl) {
scrollParentEl.on('scroll', positionPopup);
}
} else {
scrollParentEl = null;
}
angular.element($window).on('resize', positionPopup);
}, 0, false);
} else {
$scope.isOpen = false;
}
} else {
$document.off('click', documentClickBind);
if (scrollParentEl) {
scrollParentEl.off('scroll', positionPopup);
}
angular.element($window).off('resize', positionPopup);
}
});
function cameltoDash(string) {
return string.replace(/([A-Z])/g, function($1) { return '-' + $1.toLowerCase(); });
}
function parseDateString(viewValue) {
var date = dateParser.parse(viewValue, dateFormat, $scope.date);
if (isNaN(date)) {
for (var i = 0; i < altInputFormats.length; i++) {
date = dateParser.parse(viewValue, altInputFormats[i], $scope.date);
if (!isNaN(date)) {
return date;
}
}
}
return date;
}
function parseDate(viewValue) {
if (angular.isNumber(viewValue)) {
// presumably timestamp to date object
viewValue = new Date(viewValue);
}
if (!viewValue) {
return null;
}
if (angular.isDate(viewValue) && !isNaN(viewValue)) {
return viewValue;
}
if (angular.isString(viewValue)) {
var date = parseDateString(viewValue);
if (!isNaN(date)) {
return dateParser.toTimezone(date, ngModelOptions.getOption('timezone'));
}
}
return ngModelOptions.getOption('allowInvalid') ? viewValue : undefined;
}
function validator(modelValue, viewValue) {
var value = modelValue || viewValue;
if (!$attrs.ngRequired && !value) {
return true;
}
if (angular.isNumber(value)) {
value = new Date(value);
}
if (!value) {
return true;
}
if (angular.isDate(value) && !isNaN(value)) {
return true;
}
if (angular.isString(value)) {
return !isNaN(parseDateString(value));
}
return false;
}
function documentClickBind(event) {
if (!$scope.isOpen && $scope.disabled) {
return;
}
var popup = $popup[0];
var dpContainsTarget = $element[0].contains(event.target);
// The popup node may not be an element node
// In some browsers (IE) only element nodes have the 'contains' function
var popupContainsTarget = popup.contains !== undefined && popup.contains(event.target);
if ($scope.isOpen && !(dpContainsTarget || popupContainsTarget)) {
$scope.$apply(function() {
$scope.isOpen = false;
});
}
}
function inputKeydownBind(evt) {
if (evt.which === 27 && $scope.isOpen) {
evt.preventDefault();
evt.stopPropagation();
$scope.$apply(function() {
$scope.isOpen = false;
});
$element[0].focus();
} else if (evt.which === 40 && !$scope.isOpen) {
evt.preventDefault();
evt.stopPropagation();
$scope.$apply(function() {
$scope.isOpen = true;
});
}
}
function positionPopup() {
if ($scope.isOpen) {
var dpElement = angular.element($popup[0].querySelector('.uib-datepicker-popup'));
var placement = $attrs.popupPlacement ? $attrs.popupPlacement : datepickerPopupConfig.placement;
var position = $position.positionElements($element, dpElement, placement, appendToBody);
dpElement.css({top: position.top + 'px', left: position.left + 'px'});
if (dpElement.hasClass('uib-position-measure')) {
dpElement.removeClass('uib-position-measure');
}
}
}
function extractOptions(ngModelCtrl) {
var ngModelOptions;
if (angular.version.minor < 6) { // in angular < 1.6 $options could be missing
// guarantee a value
ngModelOptions = angular.isObject(ngModelCtrl.$options) ?
ngModelCtrl.$options :
{
timezone: null
};
// mimic 1.6+ api
ngModelOptions.getOption = function (key) {
return ngModelOptions[key];
};
} else { // in angular >=1.6 $options is always present
ngModelOptions = ngModelCtrl.$options;
}
return ngModelOptions;
}
$scope.$on('uib:datepicker.mode', function() {
$timeout(positionPopup, 0, false);
});
}])
.directive('uibDatepickerPopup', function() {
return {
require: ['ngModel', 'uibDatepickerPopup'],
controller: 'UibDatepickerPopupController',
scope: {
datepickerOptions: '=?',
isOpen: '=?',
currentText: '@',
clearText: '@',
closeText: '@'
},
link: function(scope, element, attrs, ctrls) {
var ngModel = ctrls[0],
ctrl = ctrls[1];
ctrl.init(ngModel);
}
};
})
.directive('uibDatepickerPopupWrap', function() {
return {
restrict: 'A',
transclude: true,
templateUrl: function(element, attrs) {
return attrs.templateUrl || 'uib/template/datepickerPopup/popup.html';
}
};
});
================================================
FILE: src/datepickerPopup/test/popup.spec.js
================================================
describe('datepicker popup', function() {
var inputEl, dropdownEl, $compile, $document, $rootScope, $sniffer,
$templateCache, $timeout;
beforeEach(module('ui.bootstrap.datepickerPopup'));
beforeEach(module('uib/template/datepicker/datepicker.html'));
beforeEach(module('uib/template/datepicker/day.html'));
beforeEach(module('uib/template/datepicker/month.html'));
beforeEach(module('uib/template/datepicker/year.html'));
beforeEach(module('uib/template/datepickerPopup/popup.html'));
beforeEach(inject(function(_$compile_, _$rootScope_, _$templateCache_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
$rootScope.date = new Date('September 30, 2010 15:30:00');
$templateCache = _$templateCache_;
}));
function getTitleButton() {
return element.find('th').eq(1).find('button').first();
}
function getTitle() {
return getTitleButton().text();
}
function clickTitleButton() {
getTitleButton().click();
}
function getLabelsRow() {
return element.find('thead').find('tr').eq(1);
}
function getLabels(dayMode) {
var els = getLabelsRow().find('th'),
labels = [];
for (var i = dayMode ? 1 : 0, n = els.length; i < n; i++) {
labels.push(els.eq(i).text());
}
return labels;
}
function getOptions(dayMode) {
var tr = element.find('tbody').find('tr');
var rows = [];
for (var j = 0, numRows = tr.length; j < numRows; j++) {
var cols = tr.eq(j).find('td'), days = [];
for (var i = dayMode ? 1 : 0, n = cols.length; i < n; i++) {
days.push(cols.eq(i).find('button').text());
}
rows.push(days);
}
return rows;
}
function clickOption(index) {
getAllOptionsEl().eq(index).click();
}
function getAllOptionsEl(dayMode) {
return element.find('tbody').find('button');
}
function selectedElementIndex() {
var buttons = getAllOptionsEl();
for (var i = 0; i < buttons.length; i++) {
if (angular.element(buttons[i]).hasClass('btn-info')) {
return i;
}
}
}
function expectSelectedElement(index) {
var buttons = getAllOptionsEl();
angular.forEach( buttons, function(button, idx) {
expect(angular.element(button).hasClass('btn-info')).toBe(idx === index);
});
}
function getSelectedElement(index) {
var buttons = getAllOptionsEl();
var el = $.grep(buttons, function(button, idx) {
return angular.element(button).hasClass('btn-info');
})[0];
return angular.element(el);
}
function triggerKeyDown(element, key, ctrl) {
var keyCodes = {
'enter': 13,
'space': 32,
'pageup': 33,
'pagedown': 34,
'end': 35,
'home': 36,
'left': 37,
'up': 38,
'right': 39,
'down': 40,
'esc': 27
};
var e = $.Event('keydown');
e.which = keyCodes[key];
if (ctrl) {
e.ctrlKey = true;
}
element.trigger(e);
}
function assignElements(wrapElement) {
inputEl = wrapElement.find('input');
dropdownEl = wrapElement.find('ul');
element = dropdownEl.find('table');
}
function changeInputValueTo(el, value) {
el.val(value);
el.trigger($sniffer.hasEvent('input') ? 'input' : 'change');
$rootScope.$digest();
}
describe('basic', function() {
var wrapElement, inputEl, dropdownEl;
function assignElements(wrapElement) {
inputEl = wrapElement.find('input');
dropdownEl = wrapElement.find('ul');
element = dropdownEl.find('table');
}
beforeEach(function() {
$rootScope.date = new Date('September 30, 2010 15:30:00');
$rootScope.isopen = true;
wrapElement = $compile('')($rootScope);
$rootScope.$digest();
assignElements(wrapElement);
});
it('should stop click event from bubbling from today button', function() {
var bubbled = false;
wrapElement.on('click', function() {
bubbled = true;
});
wrapElement.find('.uib-datepicker-current').trigger('click');
expect(bubbled).toBe(false);
});
it('should stop click event from bubbling from clear button', function() {
var bubbled = false;
wrapElement.on('click', function() {
bubbled = true;
});
wrapElement.find('.uib-clear').trigger('click');
expect(bubbled).toBe(false);
});
it('should stop click event from bubbling from close button', function() {
var bubbled = false;
wrapElement.on('click', function() {
bubbled = true;
});
wrapElement.find('.uib-close').trigger('click');
expect(bubbled).toBe(false);
});
});
describe('ngModelOptions allowInvalid', function() {
beforeEach(inject(function(_$sniffer_) {
$sniffer = _$sniffer_;
$rootScope.date = new Date('September 30, 2010 15:30:00');
$rootScope.ngModelOptions = {
allowInvalid: true
};
element = $compile('')($rootScope);
inputEl = element.find('input');
$rootScope.$digest();
}));
function changeInputValueTo(el, value) {
el.val(value);
el.trigger($sniffer.hasEvent('input') ? 'input' : 'change');
$rootScope.$digest();
}
it('should update ng-model even if the date is invalid when allowInvalid is true', function() {
changeInputValueTo(inputEl, 'pizza');
expect($rootScope.date).toBe('pizza');
expect(inputEl.val()).toBe('pizza');
});
});
describe('setting datepickerPopupConfig', function() {
var originalConfig = {};
beforeEach(inject(function(uibDatepickerPopupConfig) {
angular.extend(originalConfig, uibDatepickerPopupConfig);
uibDatepickerPopupConfig.datepickerPopup = 'MM-dd-yyyy';
element = $compile('')($rootScope);
$rootScope.$digest();
}));
afterEach(inject(function(uibDatepickerPopupConfig) {
// return it to the original state
angular.extend(uibDatepickerPopupConfig, originalConfig);
}));
it('changes date format', function() {
expect(element.val()).toEqual('09-30-2010');
});
});
describe('setting datepickerPopupConfig inside ng-if', function() {
var originalConfig = {};
beforeEach(inject(function(uibDatepickerPopupConfig) {
angular.extend(originalConfig, uibDatepickerPopupConfig);
uibDatepickerPopupConfig.datepickerPopup = 'MM-dd-yyyy';
element = $compile('')($rootScope);
$rootScope.$digest();
}));
afterEach(inject(function(uibDatepickerPopupConfig) {
// return it to the original state
angular.extend(uibDatepickerPopupConfig, originalConfig);
}));
it('changes date format', function() {
expect(element.find('input').val()).toEqual('09-30-2010');
});
});
describe('initially', function() {
beforeEach(inject(function(_$document_, _$sniffer_) {
$document = _$document_;
$sniffer = _$sniffer_;
$rootScope.isopen = true;
$rootScope.date = new Date('September 30, 2010 15:30:00');
var wrapElement = $compile('')($rootScope);
$rootScope.$digest();
assignElements(wrapElement);
}));
it('does not to display datepicker initially', function() {
expect(dropdownEl.length).toBe(0);
});
it('to display the correct value in input', function() {
expect(inputEl.val()).toBe('2010-09-30');
});
});
describe('initially opened', function() {
var wrapElement;
beforeEach(inject(function(_$document_, _$sniffer_, _$timeout_) {
$document = _$document_;
$sniffer = _$sniffer_;
$timeout = _$timeout_;
$rootScope.isopen = true;
$rootScope.date = new Date('September 30, 2010 15:30:00');
wrapElement = $compile('
')($rootScope);
$rootScope.$digest();
assignElements(wrapElement);
}));
it('datepicker is displayed', function() {
expect(dropdownEl.length).toBe(1);
});
it('renders the calendar correctly', function() {
expect(getLabelsRow().css('display')).not.toBe('none');
expect(getLabels(true)).toEqual(['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']);
expect(getOptions(true)).toEqual([
['29', '30', '31', '01', '02', '03', '04'],
['05', '06', '07', '08', '09', '10', '11'],
['12', '13', '14', '15', '16', '17', '18'],
['19', '20', '21', '22', '23', '24', '25'],
['26', '27', '28', '29', '30', '01', '02'],
['03', '04', '05', '06', '07', '08', '09']
]);
});
it('updates the input when a day is clicked', function() {
clickOption(17);
expect(inputEl.val()).toBe('2010-09-15');
expect($rootScope.date).toEqual(new Date('September 15, 2010 15:30:00'));
});
it('should mark the input field dirty when a day is clicked', function() {
expect(inputEl).toHaveClass('ng-pristine');
clickOption(17);
expect(inputEl).toHaveClass('ng-dirty');
});
it('updates the input correctly when model changes', function() {
$rootScope.date = new Date('January 10, 1983 10:00:00');
$rootScope.$digest();
expect(inputEl.val()).toBe('1983-01-10');
});
it('closes the dropdown when a day is clicked', function() {
expect(dropdownEl.length).toBe(1);
clickOption(17);
assignElements(wrapElement);
expect(dropdownEl.length).toBe(0);
});
it('updates the model & calendar when input value changes', function() {
changeInputValueTo(inputEl, '2010-09-15');
expect($rootScope.date.getFullYear()).toEqual(2010);
expect($rootScope.date.getMonth()).toEqual(8);
expect($rootScope.date.getDate()).toEqual(15);
expect(getOptions(true)).toEqual([
['29', '30', '31', '01', '02', '03', '04'],
['05', '06', '07', '08', '09', '10', '11'],
['12', '13', '14', '15', '16', '17', '18'],
['19', '20', '21', '22', '23', '24', '25'],
['26', '27', '28', '29', '30', '01', '02'],
['03', '04', '05', '06', '07', '08', '09']
]);
expectSelectedElement(17);
});
it('closes when click outside of calendar', function() {
expect(dropdownEl.length).toBe(1);
$timeout.flush(0);
$document.find('body').click();
assignElements(wrapElement);
expect(dropdownEl.length).toBe(0);
});
it('sets `ng-invalid` for invalid input', function() {
changeInputValueTo(inputEl, 'pizza');
expect(inputEl).toHaveClass('ng-invalid');
expect(inputEl).toHaveClass('ng-invalid-date');
expect($rootScope.date).toBeUndefined();
expect(inputEl.val()).toBe('pizza');
});
it('unsets `ng-invalid` for valid input', function() {
changeInputValueTo(inputEl, 'pizza');
expect(inputEl).toHaveClass('ng-invalid-date');
$rootScope.date = new Date('August 11, 2013');
$rootScope.$digest();
expect(inputEl).not.toHaveClass('ng-invalid');
expect(inputEl).not.toHaveClass('ng-invalid-date');
});
describe('focus', function () {
beforeEach(function() {
var body = $document.find('body');
body.append(inputEl);
body.append(dropdownEl);
});
afterEach(function() {
inputEl.remove();
dropdownEl.remove();
});
it('returns to the input when ESC key is pressed in the popup and closes', function() {
expect(dropdownEl.length).toBe(1);
dropdownEl.find('button').eq(0).focus();
expect(document.activeElement.tagName).toBe('BUTTON');
triggerKeyDown(dropdownEl, 'esc');
assignElements(wrapElement);
expect(dropdownEl.length).toBe(0);
expect(document.activeElement.tagName).toBe('INPUT');
});
it('returns to the input when ESC key is pressed in the input and closes', function() {
expect(dropdownEl.length).toBe(1);
dropdownEl.find('button').eq(0).focus();
expect(document.activeElement.tagName).toBe('BUTTON');
triggerKeyDown(inputEl, 'esc');
$rootScope.$digest();
assignElements(wrapElement);
expect(dropdownEl.length).toBe(0);
expect(document.activeElement.tagName).toBe('INPUT');
});
it('stops the ESC key from propagating if the dropdown is open, but not when closed', function() {
var documentKey = -1;
var getKey = function(evt) { documentKey = evt.which; };
$document.on('keydown', getKey);
triggerKeyDown(inputEl, 'esc');
expect(documentKey).toBe(-1);
triggerKeyDown(inputEl, 'esc');
expect(documentKey).toBe(27);
$document.off('keydown', getKey);
});
});
describe('works with HTML5 date input types', function() {
var date2 = new Date('October 1, 2010 12:34:56.789');
beforeEach(inject(function(_$document_) {
$document = _$document_;
$rootScope.isopen = true;
$rootScope.date = new Date('September 30, 2010 15:30:00');
}));
it('works as date', function() {
setupInputWithType('date');
expect(dropdownEl.length).toBe(1);
expect(inputEl.val()).toBe('2010-09-30');
changeInputValueTo(inputEl, '1980-03-05');
expect($rootScope.date.getFullYear()).toEqual(1980);
expect($rootScope.date.getMonth()).toEqual(2);
expect($rootScope.date.getDate()).toEqual(5);
expect(getOptions(true)).toEqual([
['24', '25', '26', '27', '28', '29', '01'],
['02', '03', '04', '05', '06', '07', '08'],
['09', '10', '11', '12', '13', '14', '15'],
['16', '17', '18', '19', '20', '21', '22'],
['23', '24', '25', '26', '27', '28', '29'],
['30', '31', '01', '02', '03', '04', '05']
]);
expect(selectedElementIndex()).toEqual(10);
});
it('works as datetime-local', function() {
setupInputWithType('datetime-local');
expect(inputEl.val()).toBe('2010-09-30T15:30:00.000');
changeInputValueTo(inputEl, '1980-03-05T12:34:56.000');
expect($rootScope.date.getFullYear()).toEqual(1980);
expect($rootScope.date.getMonth()).toEqual(2);
expect($rootScope.date.getDate()).toEqual(5);
expect(getOptions(true)).toEqual([
['24', '25', '26', '27', '28', '29', '01'],
['02', '03', '04', '05', '06', '07', '08'],
['09', '10', '11', '12', '13', '14', '15'],
['16', '17', '18', '19', '20', '21', '22'],
['23', '24', '25', '26', '27', '28', '29'],
['30', '31', '01', '02', '03', '04', '05']
]);
expect(selectedElementIndex()).toEqual(10);
});
it('works as month', function() {
setupInputWithType('month');
expect(inputEl.val()).toBe('2010-09');
changeInputValueTo(inputEl, '1980-03');
expect($rootScope.date.getFullYear()).toEqual(1980);
expect($rootScope.date.getMonth()).toEqual(2);
expect($rootScope.date.getDate()).toEqual(30);
expect(getOptions()).toEqual([
['January', 'February', 'March'],
['April', 'May', 'June'],
['July', 'August', 'September'],
['October', 'November', 'December']
]);
expect(selectedElementIndex()).toEqual(2);
});
function setupInputWithType(type) {
var wrapElement = $compile('
')($rootScope);
$rootScope.$digest();
assignElements(wrapElement);
}
});
});
describe('works with ngModelOptions', function() {
var $timeout;
beforeEach(inject(function(_$document_, _$sniffer_, _$timeout_) {
$document = _$document_;
$timeout = _$timeout_;
$rootScope.isopen = true;
$rootScope.date = new Date('September 30, 2010 15:30:00');
var wrapElement = $compile('
')($rootScope);
$rootScope.$digest();
assignElements(wrapElement);
}));
it('should change model and update calendar after debounce timeout', function() {
changeInputValueTo(inputEl, '1980-03-05');
expect($rootScope.date.getFullYear()).toEqual(2010);
expect($rootScope.date.getMonth()).toEqual(9 - 1);
expect($rootScope.date.getDate()).toEqual(30);
expect(getOptions(true)).toEqual([
['29', '30', '31', '01', '02', '03', '04'],
['05', '06', '07', '08', '09', '10', '11'],
['12', '13', '14', '15', '16', '17', '18'],
['19', '20', '21', '22', '23', '24', '25'],
['26', '27', '28', '29', '30', '01', '02'],
['03', '04', '05', '06', '07', '08', '09']
]);
// No changes yet
$timeout.flush(2000);
expect($rootScope.date.getFullYear()).toEqual(2010);
expect($rootScope.date.getMonth()).toEqual(9 - 1);
expect($rootScope.date.getDate()).toEqual(30);
expect(getOptions(true)).toEqual([
['29', '30', '31', '01', '02', '03', '04'],
['05', '06', '07', '08', '09', '10', '11'],
['12', '13', '14', '15', '16', '17', '18'],
['19', '20', '21', '22', '23', '24', '25'],
['26', '27', '28', '29', '30', '01', '02'],
['03', '04', '05', '06', '07', '08', '09']
]);
$timeout.flush(10000);
expect($rootScope.date.getFullYear()).toEqual(1980);
expect($rootScope.date.getMonth()).toEqual(2);
expect($rootScope.date.getDate()).toEqual(5);
expect(getOptions(true)).toEqual([
['24', '25', '26', '27', '28', '29', '01'],
['02', '03', '04', '05', '06', '07', '08'],
['09', '10', '11', '12', '13', '14', '15'],
['16', '17', '18', '19', '20', '21', '22'],
['23', '24', '25', '26', '27', '28', '29'],
['30', '31', '01', '02', '03', '04', '05']
]);
expectSelectedElement( 10 );
});
});
describe('works with ngModelOptions updateOn : "default"', function() {
var $timeout, wrapElement;
beforeEach(inject(function(_$document_, _$sniffer_, _$timeout_) {
$document = _$document_;
$timeout = _$timeout_;
$rootScope.isopen = true;
$rootScope.date = new Date('2010-09-30T10:00:00.000Z');
$rootScope.options = {
ngModelOptions: {
updateOn: 'default'
}
};
wrapElement = $compile('
')($rootScope);
$rootScope.$digest();
assignElements(wrapElement);
}));
it('should close the popup and update the input when a day is clicked', function() {
clickOption(17);
assignElements(wrapElement);
expect(dropdownEl.length).toBe(0);
expect(inputEl.val()).toBe('2010-09-15');
expect($rootScope.date).toEqual(new Date('2010-09-15T10:00:00.000Z'));
});
});
describe('attribute `datepickerOptions`', function() {
describe('show-weeks', function() {
beforeEach(function() {
$rootScope.opts = {
showWeeks: false
};
var wrapElement = $compile('
')($rootScope);
$rootScope.$digest();
assignElements(wrapElement);
});
it('hides week numbers based on variable', function() {
expect(getLabelsRow().find('th').length).toEqual(7);
var tr = element.find('tbody').find('tr');
for (var i = 0; i < 5; i++) {
expect(tr.eq(i).find('td').length).toEqual(7);
}
});
});
describe('init-date', function(){
beforeEach(function() {
$rootScope.date = null;
$rootScope.opts = {
initDate: new Date('November 9, 1980')
};
var wrapElement = $compile('
')($rootScope);
$rootScope.$digest();
assignElements(wrapElement);
});
it('does not alter the model', function() {
expect($rootScope.date).toBe(null);
});
it('shows the correct title', function() {
expect(getTitle()).toBe('November 1980');
});
});
describe('min-date', function() {
it('should be able to specify a min-date through options', function() {
$rootScope.opts = {
minDate: new Date('September 12, 2010'),
shortcutPropagation: 'dog'
};
var wrapElement = $compile('
')($rootScope);
$rootScope.$digest();
assignElements(wrapElement);
var buttons = getAllOptionsEl();
angular.forEach(buttons, function(button, index) {
expect(angular.element(button).prop('disabled')).toBe(index < 14);
});
$rootScope.opts.minDate = new Date('September 13, 2010');
$rootScope.$digest();
buttons = getAllOptionsEl();
angular.forEach(buttons, function(button, index) {
expect(angular.element(button).prop('disabled')).toBe(index < 15);
});
});
});
describe('max-date', function() {
it('should be able to specify a max-date through options', function() {
$rootScope.opts = {
maxDate: new Date('September 25, 2010')
};
var wrapElement = $compile('
')($rootScope);
$rootScope.$digest();
assignElements(wrapElement);
var buttons = getAllOptionsEl();
angular.forEach(buttons, function(button, index) {
expect(angular.element(button).prop('disabled')).toBe(index > 27);
});
$rootScope.opts.maxDate = new Date('September 15, 2010');
$rootScope.$digest();
buttons = getAllOptionsEl();
angular.forEach(buttons, function(button, index) {
expect(angular.element(button).prop('disabled')).toBe(index > 17);
});
});
});
describe('min-mode', function() {
it('should be able to specify min-mode through options', function() {
$rootScope.opts = {
minMode: 'month'
};
var wrapElement = $compile('
')($rootScope);
$rootScope.$digest();
assignElements(wrapElement);
expect(getTitle()).toBe('2010');
});
});
describe('max-mode', function() {
it('should be able to specify max-mode through options', function() {
$rootScope.opts = {
maxMode: 'month'
};
var wrapElement = $compile('
')($rootScope);
$rootScope.$digest();
assignElements(wrapElement);
expect(getTitle()).toBe('September 2010');
clickTitleButton();
assignElements(wrapElement);
expect(getTitle()).toBe('2010');
clickTitleButton();
assignElements(wrapElement);
expect(getTitle()).toBe('2010');
});
});
describe('datepicker-mode', function() {
beforeEach(inject(function() {
$rootScope.date = new Date('August 11, 2013');
$rootScope.opts = {
datepickerMode: 'month'
};
var wrapElement = $compile('
')($rootScope);
$rootScope.$digest();
assignElements(wrapElement);
}));
it('shows the correct title', function() {
expect(getTitle()).toBe('2013');
});
it('updates binding', function() {
clickTitleButton();
expect($rootScope.opts.datepickerMode).toBe('year');
});
});
});
describe('option `init-date`', function() {
beforeEach(function() {
$rootScope.date = null;
$rootScope.options = {
initDate: new Date('November 9, 1980')
};
});
describe('when initially set', function() {
beforeEach(function() {
var wrapElement = $compile('
')($rootScope);
$rootScope.$digest();
assignElements(wrapElement);
});
it('does not alter the model', function() {
expect($rootScope.date).toBe(null);
});
it('shows the correct title', function() {
expect(getTitle()).toBe('November 1980');
});
});
describe('when modified before date selected.', function() {
beforeEach(function() {
var wrapElement = $compile('
')($rootScope);
$rootScope.$digest();
assignElements(wrapElement);
$rootScope.options.initDate = new Date('December 20, 1981');
$rootScope.$digest();
});
it('does not alter the model', function() {
expect($rootScope.date).toBe(null);
});
it('shows the correct title', function() {
expect(getTitle()).toBe('December 1981');
});
});
describe('when modified after date selected.', function() {
beforeEach(function() {
var wrapElement = $compile('
')($rootScope);
$rootScope.$digest();
assignElements(wrapElement);
$rootScope.date = new Date('April 1, 1982');
$rootScope.options.initDate = new Date('December 20, 1981');
$rootScope.$digest();
});
it('does not alter the model', function() {
expect($rootScope.date).toEqual(new Date('April 1, 1982'));
});
it('shows the correct title', function() {
expect(getTitle()).toBe('April 1982');
});
});
});
describe('toggles programatically by `open` attribute', function() {
var wrapElement;
beforeEach(inject(function() {
$rootScope.open = true;
wrapElement = $compile('
')($rootScope);
$rootScope.$digest();
assignElements(wrapElement);
}));
it('to display initially', function() {
expect(dropdownEl.length).toBe(1);
});
it('to close / open from scope variable', function() {
expect(dropdownEl.length).toBe(1);
$rootScope.open = false;
$rootScope.$digest();
assignElements(wrapElement);
expect(dropdownEl.length).toBe(0);
$rootScope.open = true;
$rootScope.$digest();
assignElements(wrapElement);
expect(dropdownEl.length).toBe(1);
});
});
describe('custom format', function() {
beforeEach(inject(function() {
var wrapElement = $compile('
')($rootScope);
$rootScope.$digest();
assignElements(wrapElement);
}));
it('to display the correct value in input', function() {
expect(inputEl.val()).toBe('30-September-2010');
});
it('updates the input when a day is clicked', function() {
clickOption(17);
expect(inputEl.val()).toBe('15-September-2010');
expect($rootScope.date).toEqual(new Date('September 15, 2010 15:30:00'));
});
it('updates the input correctly when model changes', function() {
$rootScope.date = new Date('January 10, 1983 10:00:00');
$rootScope.$digest();
expect(inputEl.val()).toBe('10-January-1983');
});
});
describe('custom format with time', function() {
beforeEach(inject(function() {
var wrapElement = $compile('
')($rootScope);
$rootScope.$digest();
assignElements(wrapElement);
}));
it('updates the model correctly when the input value changes', function() {
$rootScope.date = new Date(2015, 10, 24, 10, 0);
$rootScope.$digest();
expect(inputEl.val()).toBe('Nov-24-2015 10:00 AM');
inputEl.val('Nov-24-2015 11:00 AM').trigger('input');
$rootScope.$digest();
expect($rootScope.date).toEqual(new Date(2015, 10, 24, 11, 0));
});
});
describe('custom format with optional leading zeroes', function() {
beforeEach(inject(function() {
var wrapElement = $compile('
')($rootScope);
$rootScope.$digest();
assignElements(wrapElement);
}));
it('to display the correct value in input', function() {
expect(inputEl.val()).toBe('30-09-2010');
});
it('updates the input when a day is clicked', function() {
clickOption(10);
expect(inputEl.val()).toBe('08-09-2010');
expect($rootScope.date).toEqual(new Date('September 8, 2010 15:30:00'));
});
it('updates the input correctly when model changes', function() {
$rootScope.date = new Date('December 25, 1983 10:00:00');
$rootScope.$digest();
expect(inputEl.val()).toBe('25-12-1983');
});
});
describe('dynamic custom format', function() {
beforeEach(inject(function() {
$rootScope.format = 'dd-MMMM-yyyy';
var wrapElement = $compile('
')($rootScope);
$rootScope.$digest();
assignElements(wrapElement);
}));
it('to display the correct value in input', function() {
expect(inputEl.val()).toBe('30-September-2010');
});
it('updates the input when a day is clicked', function() {
clickOption(17);
expect(inputEl.val()).toBe('15-September-2010');
expect($rootScope.date).toEqual(new Date('September 15, 2010 15:30:00'));
});
it('updates the input correctly when model changes', function() {
$rootScope.date = new Date('August 11, 2013 09:09:00');
$rootScope.$digest();
expect(inputEl.val()).toBe('11-August-2013');
});
it('updates the input correctly when format changes', function() {
$rootScope.format = 'dd/MM/yyyy';
$rootScope.$digest();
expect(inputEl.val()).toBe('30/09/2010');
});
});
describe('format errors', function() {
var originalConfig = {};
beforeEach(inject(function(uibDatepickerPopupConfig) {
angular.extend(originalConfig, uibDatepickerPopupConfig);
uibDatepickerPopupConfig.datepickerPopup = null;
}));
afterEach(inject(function(uibDatepickerPopupConfig) {
// return it to the original state
angular.extend(uibDatepickerPopupConfig, originalConfig);
}));
it('should throw an error if there is no format', function() {
expect(function() {
$compile('
')($rootScope);
}).toThrow(new Error('uibDatepickerPopup must have a date format specified.'));
});
it('should throw an error if the format changes to null without fallback', function() {
$rootScope.format = 'dd-MMMM-yyyy';
$compile('
')($rootScope);
$rootScope.$digest();
expect(function() {
$rootScope.format = null;
$rootScope.$digest();
}).toThrow(new Error('uibDatepickerPopup must have a date format specified.'));
});
it('should thrown an error on date inputs with custom formats', function() {
expect(function() {
$compile('
')($rootScope);
}).toThrow(new Error('HTML5 date input types do not support custom formats.'));
});
});
describe('european format', function() {
it('dd.MM.yyyy', function() {
var wrapElement = $compile('
')($rootScope);
$rootScope.$digest();
assignElements(wrapElement);
changeInputValueTo(inputEl, '11.08.2013');
expect($rootScope.date.getFullYear()).toEqual(2013);
expect($rootScope.date.getMonth()).toEqual(7);
expect($rootScope.date.getDate()).toEqual(11);
});
});
describe('`close-on-date-selection` attribute', function() {
var wrapElement;
beforeEach(inject(function() {
$rootScope.close = false;
wrapElement = $compile('
')($rootScope);
$rootScope.$digest();
assignElements(wrapElement);
}));
it('does not close the dropdown when a day is clicked', function() {
clickOption(17);
assignElements(wrapElement);
expect(dropdownEl.length).toBe(1);
});
});
describe('button bar', function() {
var buttons, buttonBarElement;
function assignButtonBar() {
buttonBarElement = dropdownEl.find('li').eq(-1);
buttons = buttonBarElement.find('button');
}
describe('', function() {
var wrapElement;
beforeEach(inject(function() {
$rootScope.isopen = true;
wrapElement = $compile('
')($rootScope);
$rootScope.$digest();
assignElements(wrapElement);
assignButtonBar();
}));
it('should exist', function() {
expect(dropdownEl.length).toBe(1);
expect(dropdownEl.find('li').length).toBe(2);
});
it('should have three buttons', function() {
expect(buttons.length).toBe(3);
expect(buttons.eq(0).text()).toBe('Today');
expect(buttons.eq(1).text()).toBe('Clear');
expect(buttons.eq(2).text()).toBe('Done');
});
it('should have a button to set today date without altering time part', function() {
var today = new Date();
buttons.eq(0).click();
expect($rootScope.date.getFullYear()).toBe(today.getFullYear());
expect($rootScope.date.getMonth()).toBe(today.getMonth());
expect($rootScope.date.getDate()).toBe(today.getDate());
expect($rootScope.date.getHours()).toBe(15);
expect($rootScope.date.getMinutes()).toBe(30);
expect($rootScope.date.getSeconds()).toBe(0);
});
it('should have a button to set today date if blank', function() {
$rootScope.date = null;
$rootScope.$digest();
var today = new Date();
buttons.eq(0).click();
expect($rootScope.date.getFullYear()).toBe(today.getFullYear());
expect($rootScope.date.getMonth()).toBe(today.getMonth());
expect($rootScope.date.getDate()).toBe(today.getDate());
expect($rootScope.date.getHours()).toBe(0);
expect($rootScope.date.getMinutes()).toBe(0);
expect($rootScope.date.getSeconds()).toBe(0);
});
it('should have a button to clear value', function() {
buttons.eq(1).click();
expect($rootScope.date).toBe(null);
});
it('should clear the previously selected date', function() {
$rootScope.date = new Date();
$rootScope.$digest();
buttons.eq(1).click();
expect($rootScope.date).toBe(null);
});
it('should have a button to close calendar', function() {
buttons.eq(2).click();
assignElements(wrapElement);
expect(dropdownEl.length).toBe(0);
});
});
describe('customization', function() {
it('should change text from attributes', function() {
$rootScope.clearText = 'Null it!';
$rootScope.close = 'Close';
var wrapElement = $compile('
')($rootScope);
$rootScope.$digest();
assignElements(wrapElement);
assignButtonBar();
expect(buttons.eq(0).text()).toBe('Now');
expect(buttons.eq(1).text()).toBe('Null it!');
expect(buttons.eq(2).text()).toBe('CloseME');
});
it('should disable today button if before min date', function() {
var date = new Date();
date.setDate(new Date().getDate() + 1);
$rootScope.options = {
minDate: date
};
var wrapElement = $compile('
')($rootScope);
$rootScope.$digest();
assignElements(wrapElement);
assignButtonBar();
expect(buttons.eq(0).prop('disabled')).toBe(true);
});
it('should disable today button if before min date, yyyy-MM-dd case', inject(function(dateFilter) {
var date = new Date();
date.setDate(new Date().getDate() + 1);
var literalMinDate = dateFilter(date, 'yyyy-MM-dd');
$rootScope.options = {
minDate: literalMinDate
};
var wrapElement = $compile('
')($rootScope);
$rootScope.$digest();
assignElements(wrapElement);
assignButtonBar();
expect(buttons.eq(0).prop('disabled')).toBe(true);
}));
it('should not disable any button if min date is null', function() {
$rootScope.options = {
minDate: null
};
var wrapElement = $compile('
')($rootScope);
$rootScope.$digest();
assignElements(wrapElement);
assignButtonBar();
for (var i = 0; i < buttons.length; i++) {
expect(buttons.eq(i).prop('disabled')).toBe(false);
}
});
it('should disable today button if after max date', function() {
var date = new Date();
date.setDate(new Date().getDate() - 2);
$rootScope.options = {
maxDate: date
};
var wrapElement = $compile('
')($rootScope);
$rootScope.$digest();
assignElements(wrapElement);
assignButtonBar();
expect(buttons.eq(0).prop('disabled')).toBe(true);
});
it('should not disable any button if max date is null', function() {
$rootScope.options = {
maxDate: null
};
var wrapElement = $compile('
')($rootScope);
$rootScope.$digest();
assignElements(wrapElement);
assignButtonBar();
for (var i = 0; i < buttons.length; i++) {
expect(buttons.eq(i).prop('disabled')).toBe(false);
}
});
it('should remove bar', function() {
$rootScope.showBar = false;
var wrapElement = $compile('
')($rootScope);
$rootScope.$digest();
assignElements(wrapElement);
expect(dropdownEl.find('li').length).toBe(1);
});
it('should hide weeks column on popup', function() {
$rootScope.options = {
showWeeks: false
};
var wrapElement = $compile('
')($rootScope);
$rootScope.$digest();
assignElements(wrapElement);
expect(getLabelsRow().find('th').length).toEqual(7);
var tr = element.find('tbody').find('tr');
for (var i = 0; i < 5; i++) {
expect(tr.eq(i).find('td').length).toEqual(7);
}
});
it('should show weeks column on popup', function() {
$rootScope.options = {
showWeeks: true
};
var wrapElement = $compile('
')($rootScope);
$rootScope.$digest();
assignElements(wrapElement);
expect(getLabelsRow().find('th').eq(0)).not.toBeHidden();
var tr = element.find('tbody').find('tr');
for (var i = 0; i < 5; i++) {
expect(tr.eq(i).find('td').eq(0)).not.toBeHidden();
}
});
});
describe('`ng-change`', function() {
beforeEach(inject(function() {
$rootScope.changeHandler = jasmine.createSpy('changeHandler');
var wrapElement = $compile('
')($rootScope);
$rootScope.$digest();
assignElements(wrapElement);
assignButtonBar();
}));
it('should be called when `today` is clicked', function() {
buttons.eq(0).click();
expect($rootScope.changeHandler).toHaveBeenCalled();
});
it('should be called when `clear` is clicked', function() {
buttons.eq(1).click();
expect($rootScope.changeHandler).toHaveBeenCalled();
});
it('should not be called when `close` is clicked', function() {
buttons.eq(2).click();
expect($rootScope.changeHandler).not.toHaveBeenCalled();
});
});
});
describe('use with `ng-required` directive', function() {
describe('`ng-required is true`', function() {
beforeEach(inject(function() {
$rootScope.date = '';
var wrapElement = $compile('
')($rootScope);
$rootScope.$digest();
assignElements(wrapElement);
}));
it('should be invalid initially and when no date', function() {
expect(inputEl.hasClass('ng-invalid')).toBeTruthy();
});
it('should be valid if model has been specified', function() {
$rootScope.date = new Date();
$rootScope.$digest();
expect(inputEl.hasClass('ng-valid')).toBeTruthy();
});
it('should be valid if model value is a valid timestamp', function() {
$rootScope.date = Date.now();
$rootScope.$digest();
expect(inputEl.hasClass('ng-valid')).toBeTruthy();
});
});
describe('`ng-required is false`', function() {
beforeEach(inject(function() {
$rootScope.date = '';
var wrapElement = $compile('
')($rootScope);
$rootScope.$digest();
assignElements(wrapElement);
}));
it('should be valid initially and when no date', function() {
expect(inputEl.hasClass('ng-valid')).toBeTruthy();
});
});
});
describe('use with `ng-change` directive', function() {
beforeEach(inject(function() {
$rootScope.changeHandler = jasmine.createSpy('changeHandler');
$rootScope.date = new Date('09/16/2010');
var wrapElement = $compile('
')($rootScope);
$rootScope.$digest();
assignElements(wrapElement);
}));
it('should not be called initially', function() {
expect($rootScope.changeHandler).not.toHaveBeenCalled();
});
it('should be called when a day is clicked', function() {
clickOption(17);
expect($rootScope.changeHandler).toHaveBeenCalled();
});
it('should not be called when model changes programatically', function() {
$rootScope.date = new Date();
$rootScope.$digest();
expect($rootScope.changeHandler).not.toHaveBeenCalled();
});
});
describe('with disabled', function() {
var wrapElement;
beforeEach(function() {
$rootScope.isOpen = false;
wrapElement = $compile('
')($rootScope);
$rootScope.$digest();
});
it('should not open the popup', function() {
$rootScope.isOpen = true;
$rootScope.$digest();
expect($rootScope.isOpen).toBe(false);
expect(wrapElement.find('ul').length).toBe(0);
});
});
describe('with ng-disabled', function() {
var wrapElement;
beforeEach(function() {
$rootScope.disabled = false;
$rootScope.isOpen = false;
wrapElement = $compile('
')($rootScope);
$rootScope.$digest();
});
it('should not open the popup when disabled', function() {
$rootScope.isOpen = true;
$rootScope.$digest();
expect($rootScope.isOpen).toBe(true);
expect(wrapElement.find('ul').length).toBe(1);
$rootScope.isOpen = false;
$rootScope.$digest();
expect($rootScope.isOpen).toBe(false);
expect(wrapElement.find('ul').length).toBe(0);
$rootScope.disabled = true;
$rootScope.isOpen = true;
$rootScope.$digest();
expect($rootScope.isOpen).toBe(false);
expect(wrapElement.find('ul').length).toBe(0);
$rootScope.disabled = false;
$rootScope.isOpen = true;
$rootScope.$digest();
expect($rootScope.isOpen).toBe(true);
expect(wrapElement.find('ul').length).toBe(1);
});
});
describe('with datepicker-popup-template-url', function() {
beforeEach(function() {
$rootScope.date = new Date();
});
afterEach(function () {
$document.find('body').find('.dropdown-menu').remove();
});
it('should allow custom templates for the popup', function() {
$templateCache.put('foo/bar.html', '
baz
');
var elm = angular.element('
');
$compile(elm)($rootScope);
$rootScope.$digest();
expect(elm.children().eq(1).html()).toBe('
baz
');
});
});
describe('with datepicker-template-url', function() {
beforeEach(function() {
$rootScope.date = new Date();
});
afterEach(function() {
$document.find('body').find('.dropdown-menu').remove();
});
it('should allow custom templates for the datepicker', function() {
$templateCache.put('foo/bar.html', '
baz
');
var elm = angular.element('
');
$compile(elm)($rootScope);
$rootScope.$digest();
var datepicker = elm.find('[uib-datepicker]');
expect(datepicker.html()).toBe('
baz
');
});
});
describe('with an append-to-body attribute', function() {
beforeEach(function() {
$rootScope.date = new Date();
});
afterEach(function() {
$document.find('body').children().remove();
});
it('should append to the body', function() {
var $body = $document.find('body'),
bodyLength = $body.children().length,
elm = angular.element(
'
'
);
$compile(elm)($rootScope);
$rootScope.$digest();
expect($body.children().length).toEqual(bodyLength + 1);
expect(elm.children().length).toEqual(1);
});
it('should be removed on scope destroy', function() {
var $body = $document.find('body'),
bodyLength = $body.children().length,
isolatedScope = $rootScope.$new(),
elm = angular.element(
'
'
);
$compile(elm)(isolatedScope);
isolatedScope.$digest();
expect($body.children().length).toEqual(bodyLength + 1);
isolatedScope.$destroy();
expect($body.children().length).toEqual(bodyLength);
});
});
describe('with setting datepickerConfig.showWeeks to false', function() {
var originalConfig = {};
beforeEach(inject(function(uibDatepickerConfig) {
angular.extend(originalConfig, uibDatepickerConfig);
uibDatepickerConfig.showWeeks = false;
var wrapElement = $compile('
')($rootScope);
$rootScope.$digest();
assignElements(wrapElement);
}));
afterEach(inject(function(uibDatepickerConfig) {
// return it to the original state
angular.extend(uibDatepickerConfig, originalConfig);
}));
it('changes initial visibility for weeks', function() {
expect(getLabelsRow().find('th').length).toEqual(7);
var tr = element.find('tbody').find('tr');
for (var i = 0; i < 5; i++) {
expect(tr.eq(i).find('td').length).toEqual(7);
}
});
});
describe('`datepicker-mode`', function() {
beforeEach(inject(function() {
$rootScope.date = new Date('August 11, 2013');
$rootScope.options = {
datepickerMode: 'month'
};
var wrapElement = $compile('
')($rootScope);
$rootScope.$digest();
assignElements(wrapElement);
}));
it('shows the correct title', function() {
expect(getTitle()).toBe('2013');
});
it('updates binding', function() {
clickTitleButton();
expect($rootScope.options.datepickerMode).toBe('year');
});
});
describe('attribute `onOpenFocus`', function() {
beforeEach(function() {
$rootScope.date = null;
$rootScope.isopen = false;
var wrapElement = $compile('
')($rootScope);
$rootScope.$digest();
assignElements(wrapElement);
});
it('should remain focused on the input', function() {
var focused = true;
expect(dropdownEl.length).toBe(0);
inputEl[0].focus();
inputEl.on('blur', function() {
focused = false;
});
$rootScope.isopen = true;
$rootScope.$digest();
expect(inputEl.parent().find('.dropdown-menu').length).toBe(1);
expect(focused).toBe(true);
});
});
describe('altInputFormats', function() {
describe('datepickerPopupConfig.altInputFormats', function() {
var originalConfig = {};
beforeEach(inject(function(uibDatepickerPopupConfig) {
$rootScope.date = new Date('November 9, 1980');
angular.extend(originalConfig, uibDatepickerPopupConfig);
uibDatepickerPopupConfig.datepickerPopup = 'MM-dd-yyyy';
uibDatepickerPopupConfig.altInputFormats = ['M!/d!/yyyy'];
var wrapElement = $compile('
')($rootScope);
$rootScope.$digest();
assignElements(wrapElement);
}));
afterEach(inject(function(uibDatepickerPopupConfig) {
// return it to the original state
angular.extend(uibDatepickerPopupConfig, originalConfig);
}));
it('changes date format', function() {
changeInputValueTo(inputEl, '11/8/1980');
expect($rootScope.date.getFullYear()).toEqual(1980);
expect($rootScope.date.getMonth()).toEqual(10);
expect($rootScope.date.getDate()).toEqual(8);
});
it('changes the datepicker', function() {
expect(selectedElementIndex()).toEqual(14);
changeInputValueTo(inputEl, '11/8/1980');
expect(selectedElementIndex()).toEqual(13);
});
});
describe('attribute `alt-input-formats`', function() {
beforeEach(function() {
$rootScope.date = new Date('November 9, 1980');
var wrapElement = $compile('
')($rootScope);
$rootScope.$digest();
assignElements(wrapElement);
});
it('should accept alternate input formats', function() {
changeInputValueTo(inputEl, '11/8/1980');
expect($rootScope.date.getFullYear()).toEqual(1980);
expect($rootScope.date.getMonth()).toEqual(10);
expect($rootScope.date.getDate()).toEqual(8);
});
it('changes the datepicker', function() {
expect(selectedElementIndex()).toEqual(14);
changeInputValueTo(inputEl, '11/8/1980');
expect(selectedElementIndex()).toEqual(13);
});
});
});
describe('uibDatepickerConfig ngModelOptions', function() {
var inputEl, dropdownEl;
function assignElements(wrapElement) {
inputEl = wrapElement.find('input');
dropdownEl = wrapElement.find('ul');
element = dropdownEl.find('table');
}
beforeEach(inject(function(uibDatepickerConfig) {
uibDatepickerConfig.ngModelOptions = { timezone: '+600' };
$rootScope.date = new Date('2010-09-30T10:00:00.000Z');
$rootScope.isopen = true;
}));
afterEach(inject(function(uibDatepickerConfig) {
uibDatepickerConfig.ngModelOptions = {};
}));
describe('timezone', function() {
beforeEach(inject(function(uibDatepickerConfig) {
var wrapper = $compile('
')($rootScope);
$rootScope.$digest();
assignElements(wrapper);
}));
it('interprets the date appropriately', function() {
expect(inputEl.val()).toBe('09/30/2010');
});
it('updates the input when a day is clicked', function() {
clickOption(17);
expect(inputEl.val()).toBe('09/15/2010');
expect($rootScope.date).toEqual(new Date('2010-09-15T10:00:00.000Z'));
});
it('shows the correct title', function() {
expect(getTitle()).toBe('September 2010');
});
});
it('timezone interprets init date appropriately', function() {
$rootScope.options = {
initDate: new Date('2010-09-30T23:00:00.000Z')
};
$rootScope.date = null;
var wrapper = $compile('
')($rootScope);
$rootScope.$digest();
assignElements(wrapper);
expect(getTitle()).toBe('October 2010');
});
it('timezone interprets min date appropriately', function() {
$rootScope.options = {
minDate: new Date('2010-10-01T00:00:00.000Z')
};
var wrapper = $compile('
')($rootScope);
$rootScope.$digest();
assignElements(wrapper);
expect(getSelectedElement().prop('disabled')).toBe(true);
});
});
describe('ng-model-options', function() {
describe('timezone', function() {
var inputEl, dropdownEl, $document, $sniffer, $timeout;
function assignElements(wrapElement) {
inputEl = wrapElement.find('input');
dropdownEl = wrapElement.find('ul');
element = dropdownEl.find('table');
}
beforeEach(function() {
$rootScope.date = new Date('2010-09-30T10:00:00.000Z');
$rootScope.options = {
ngModelOptions: {
timezone: '+600'
}
};
$rootScope.isopen = true;
var wrapper = $compile('
')($rootScope);
$rootScope.$digest();
assignElements(wrapper);
});
it('interprets the date appropriately', function() {
expect(inputEl.val()).toBe('09/30/2010');
});
it('has `selected` only the correct day', function() {
expectSelectedElement(32);
});
it('updates the input when a day is clicked', function() {
clickOption(17);
expect(inputEl.val()).toBe('09/15/2010');
expect($rootScope.date).toEqual(new Date('2010-09-15T10:00:00.000Z'));
});
});
describe('timezone HTML5 date input', function() {
var inputEl, dropdownEl, $document, $sniffer, $timeout;
function assignElements(wrapElement) {
inputEl = wrapElement.find('input');
dropdownEl = wrapElement.find('ul');
element = dropdownEl.find('table');
}
beforeEach(function() {
$rootScope.date = new Date('2010-09-30T10:00:00.000Z');
$rootScope.options = {
ngModelOptions: {
timezone: '+600'
}
};
$rootScope.isopen = true;
var wrapper = $compile('
')($rootScope);
$rootScope.$digest();
assignElements(wrapper);
});
it('interprets the date appropriately', function() {
expect(inputEl.val()).toBe('2010-09-30');
});
it('has `selected` only the correct day', function() {
expectSelectedElement(32);
});
it('updates the input when a day is clicked', function() {
clickOption(17);
expect(inputEl.val()).toBe('2010-09-15');
expect($rootScope.date).toEqual(new Date('2010-09-15T10:00:00.000Z'));
});
});
});
});
================================================
FILE: src/debounce/debounce.js
================================================
angular.module('ui.bootstrap.debounce', [])
/**
* A helper, internal service that debounces a function
*/
.factory('$$debounce', ['$timeout', function($timeout) {
return function(callback, debounceTime) {
var timeoutPromise;
return function() {
var self = this;
var args = Array.prototype.slice.call(arguments);
if (timeoutPromise) {
$timeout.cancel(timeoutPromise);
}
timeoutPromise = $timeout(function() {
callback.apply(self, args);
}, debounceTime);
};
};
}]);
================================================
FILE: src/debounce/index.js
================================================
require('./debounce');
var MODULE_NAME = 'ui.bootstrap.module.debounce';
angular.module(MODULE_NAME, ['ui.bootstrap.debounce']);
module.exports = MODULE_NAME;
================================================
FILE: src/debounce/test/debounce.spec.js
================================================
describe('$$debounce', function() {
var $$debounce, $timeout, debouncedFunction, i, args;
beforeEach(module('ui.bootstrap.debounce'));
beforeEach(inject(function(_$$debounce_, _$timeout_) {
$$debounce = _$$debounce_;
$timeout = _$timeout_;
i = 0;
debouncedFunction = $$debounce(function() {
args = Array.prototype.slice.call(arguments);
i++;
}, 100);
}));
it('should function like a $timeout when called once during timeout', function() {
debouncedFunction();
$timeout.flush(50);
expect(i).toBe(0);
$timeout.flush(50);
expect(i).toBe(1);
});
it('should only execute 100ms after last call when called twice', function() {
debouncedFunction();
$timeout.flush(50);
expect(i).toBe(0);
debouncedFunction();
$timeout.flush(50);
expect(i).toBe(0);
$timeout.flush(50);
expect(i).toBe(1);
});
it('should properly pass arguments to debounced function', function() {
debouncedFunction(1, 2, 3);
$timeout.flush(100);
expect(args).toEqual([1, 2, 3]);
});
});
================================================
FILE: src/dropdown/docs/demo.html
================================================
Click me for a dropdown, yo!
append-to vs. append-to-body vs. inline example
================================================
FILE: src/dropdown/docs/demo.js
================================================
angular.module('ui.bootstrap.demo').controller('DropdownCtrl', function ($scope, $log) {
$scope.items = [
'The first choice!',
'And another choice for you.',
'but wait! A third!'
];
$scope.status = {
isopen: false
};
$scope.toggled = function(open) {
$log.log('Dropdown is now: ', open);
};
$scope.toggleDropdown = function($event) {
$event.preventDefault();
$event.stopPropagation();
$scope.status.isopen = !$scope.status.isopen;
};
$scope.appendToEl = angular.element(document.querySelector('#dropdown-long-content'));
});
================================================
FILE: src/dropdown/docs/readme.md
================================================
Dropdown is a simple directive which will toggle a dropdown menu on click or programmatically.
This directive is composed by three parts:
* `uib-dropdown` which transforms a node into a dropdown.
* `uib-dropdown-toggle` which allows the dropdown to be toggled via click. This directive is optional.
* `uib-dropdown-menu` which transforms a node into the popup menu.
Each of these parts need to be used as attribute directives.
### uib-dropdown settings
* `auto-close`
_(Default: `always`)_ -
Controls the behavior of the menu when clicked.
* `always` - Automatically closes the dropdown when any of its elements is clicked.
* `disabled` - Disables the auto close. You can control it manually with `is-open`. It still gets closed if the toggle is clicked, `esc` is pressed or another dropdown is open.
* `outsideClick` - Closes the dropdown automatically only when the user clicks any element outside the dropdown.
* `dropdown-append-to`
$
_(Default: `null`)_ -
Appends the inner dropdown-menu to an arbitrary DOM element.
* `dropdown-append-to-body`
B
_(Default: `false`)_ -
Appends the inner dropdown-menu to the body element if the attribute is present without a value, or with a non `false` value.
* `is-open`
$
_(Default: `false`)_ -
Defines whether or not the dropdown-menu is open. The `uib-dropdown-toggle` will toggle this attribute on click.
* `keyboard-nav`:
B
_(Default: `false`)_ -
Enables navigation of dropdown list elements with the arrow keys.
* `on-toggle(open)`
$ -
An optional expression called when the dropdown menu is opened or closed.
### uib-dropdown-menu settings
* `template-url`
_(Default: `none`)_ -
You may specify a template for the dropdown menu. Check the demos for an example.
### Additional settings `uibDropdownConfig`
* `appendToOpenClass`
_(Default: `uib-dropdown-open`)_ -
Class to apply when the dropdown is open and appended to a different DOM element.
* `openClass`
_(Default: `open`)_ -
Class to apply when the dropdown is open.
### Known issues
For usage with ngTouch, it is recommended to use the programmatic `is-open` trigger with ng-click - this is due to ngTouch decorating ng-click to prevent propagation of the event.
================================================
FILE: src/dropdown/dropdown.js
================================================
angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.multiMap', 'ui.bootstrap.position'])
.constant('uibDropdownConfig', {
appendToOpenClass: 'uib-dropdown-open',
openClass: 'open'
})
.service('uibDropdownService', ['$document', '$rootScope', '$$multiMap', function($document, $rootScope, $$multiMap) {
var openScope = null;
var openedContainers = $$multiMap.createNew();
this.isOnlyOpen = function(dropdownScope, appendTo) {
var openedDropdowns = openedContainers.get(appendTo);
if (openedDropdowns) {
var openDropdown = openedDropdowns.reduce(function(toClose, dropdown) {
if (dropdown.scope === dropdownScope) {
return dropdown;
}
return toClose;
}, {});
if (openDropdown) {
return openedDropdowns.length === 1;
}
}
return false;
};
this.open = function(dropdownScope, element, appendTo) {
if (!openScope) {
$document.on('click', closeDropdown);
}
if (openScope && openScope !== dropdownScope) {
openScope.isOpen = false;
}
openScope = dropdownScope;
if (!appendTo) {
return;
}
var openedDropdowns = openedContainers.get(appendTo);
if (openedDropdowns) {
var openedScopes = openedDropdowns.map(function(dropdown) {
return dropdown.scope;
});
if (openedScopes.indexOf(dropdownScope) === -1) {
openedContainers.put(appendTo, {
scope: dropdownScope
});
}
} else {
openedContainers.put(appendTo, {
scope: dropdownScope
});
}
};
this.close = function(dropdownScope, element, appendTo) {
if (openScope === dropdownScope) {
$document.off('click', closeDropdown);
$document.off('keydown', this.keybindFilter);
openScope = null;
}
if (!appendTo) {
return;
}
var openedDropdowns = openedContainers.get(appendTo);
if (openedDropdowns) {
var dropdownToClose = openedDropdowns.reduce(function(toClose, dropdown) {
if (dropdown.scope === dropdownScope) {
return dropdown;
}
return toClose;
}, {});
if (dropdownToClose) {
openedContainers.remove(appendTo, dropdownToClose);
}
}
};
var closeDropdown = function(evt) {
// This method may still be called during the same mouse event that
// unbound this event handler. So check openScope before proceeding.
if (!openScope || !openScope.isOpen) { return; }
if (evt && openScope.getAutoClose() === 'disabled') { return; }
if (evt && evt.which === 3) { return; }
var toggleElement = openScope.getToggleElement();
if (evt && toggleElement && toggleElement[0].contains(evt.target)) {
return;
}
var dropdownElement = openScope.getDropdownElement();
if (evt && openScope.getAutoClose() === 'outsideClick' &&
dropdownElement && dropdownElement[0].contains(evt.target)) {
return;
}
openScope.focusToggleElement();
openScope.isOpen = false;
if (!$rootScope.$$phase) {
openScope.$apply();
}
};
this.keybindFilter = function(evt) {
if (!openScope) {
// see this.close as ESC could have been pressed which kills the scope so we can not proceed
return;
}
var dropdownElement = openScope.getDropdownElement();
var toggleElement = openScope.getToggleElement();
var dropdownElementTargeted = dropdownElement && dropdownElement[0].contains(evt.target);
var toggleElementTargeted = toggleElement && toggleElement[0].contains(evt.target);
if (evt.which === 27) {
evt.stopPropagation();
openScope.focusToggleElement();
closeDropdown();
} else if (openScope.isKeynavEnabled() && [38, 40].indexOf(evt.which) !== -1 && openScope.isOpen && (dropdownElementTargeted || toggleElementTargeted)) {
evt.preventDefault();
evt.stopPropagation();
openScope.focusDropdownEntry(evt.which);
}
};
}])
.controller('UibDropdownController', ['$scope', '$element', '$attrs', '$parse', 'uibDropdownConfig', 'uibDropdownService', '$animate', '$uibPosition', '$document', '$compile', '$templateRequest', function($scope, $element, $attrs, $parse, dropdownConfig, uibDropdownService, $animate, $position, $document, $compile, $templateRequest) {
var self = this,
scope = $scope.$new(), // create a child scope so we are not polluting original one
templateScope,
appendToOpenClass = dropdownConfig.appendToOpenClass,
openClass = dropdownConfig.openClass,
getIsOpen,
setIsOpen = angular.noop,
toggleInvoker = $attrs.onToggle ? $parse($attrs.onToggle) : angular.noop,
keynavEnabled = false,
selectedOption = null,
body = $document.find('body');
$element.addClass('dropdown');
this.init = function() {
if ($attrs.isOpen) {
getIsOpen = $parse($attrs.isOpen);
setIsOpen = getIsOpen.assign;
$scope.$watch(getIsOpen, function(value) {
scope.isOpen = !!value;
});
}
keynavEnabled = angular.isDefined($attrs.keyboardNav);
};
this.toggle = function(open) {
scope.isOpen = arguments.length ? !!open : !scope.isOpen;
if (angular.isFunction(setIsOpen)) {
setIsOpen(scope, scope.isOpen);
}
return scope.isOpen;
};
// Allow other directives to watch status
this.isOpen = function() {
return scope.isOpen;
};
scope.getToggleElement = function() {
return self.toggleElement;
};
scope.getAutoClose = function() {
return $attrs.autoClose || 'always'; //or 'outsideClick' or 'disabled'
};
scope.getElement = function() {
return $element;
};
scope.isKeynavEnabled = function() {
return keynavEnabled;
};
scope.focusDropdownEntry = function(keyCode) {
var elems = self.dropdownMenu ? //If append to body is used.
angular.element(self.dropdownMenu).find('a') :
$element.find('ul').eq(0).find('a');
switch (keyCode) {
case 40: {
if (!angular.isNumber(self.selectedOption)) {
self.selectedOption = 0;
} else {
self.selectedOption = self.selectedOption === elems.length - 1 ?
self.selectedOption :
self.selectedOption + 1;
}
break;
}
case 38: {
if (!angular.isNumber(self.selectedOption)) {
self.selectedOption = elems.length - 1;
} else {
self.selectedOption = self.selectedOption === 0 ?
0 : self.selectedOption - 1;
}
break;
}
}
elems[self.selectedOption].focus();
};
scope.getDropdownElement = function() {
return self.dropdownMenu;
};
scope.focusToggleElement = function() {
if (self.toggleElement) {
self.toggleElement[0].focus();
}
};
function removeDropdownMenu() {
$element.append(self.dropdownMenu);
}
scope.$watch('isOpen', function(isOpen, wasOpen) {
var appendTo = null,
appendToBody = false;
if (angular.isDefined($attrs.dropdownAppendTo)) {
var appendToEl = $parse($attrs.dropdownAppendTo)(scope);
if (appendToEl) {
appendTo = angular.element(appendToEl);
}
}
if (angular.isDefined($attrs.dropdownAppendToBody)) {
var appendToBodyValue = $parse($attrs.dropdownAppendToBody)(scope);
if (appendToBodyValue !== false) {
appendToBody = true;
}
}
if (appendToBody && !appendTo) {
appendTo = body;
}
if (appendTo && self.dropdownMenu) {
if (isOpen) {
appendTo.append(self.dropdownMenu);
$element.on('$destroy', removeDropdownMenu);
} else {
$element.off('$destroy', removeDropdownMenu);
removeDropdownMenu();
}
}
if (appendTo && self.dropdownMenu) {
var pos = $position.positionElements($element, self.dropdownMenu, 'bottom-left', true),
css,
rightalign,
scrollbarPadding,
scrollbarWidth = 0;
css = {
top: pos.top + 'px',
display: isOpen ? 'block' : 'none'
};
rightalign = self.dropdownMenu.hasClass('dropdown-menu-right');
if (!rightalign) {
css.left = pos.left + 'px';
css.right = 'auto';
} else {
css.left = 'auto';
scrollbarPadding = $position.scrollbarPadding(appendTo);
if (scrollbarPadding.heightOverflow && scrollbarPadding.scrollbarWidth) {
scrollbarWidth = scrollbarPadding.scrollbarWidth;
}
css.right = window.innerWidth - scrollbarWidth -
(pos.left + $element.prop('offsetWidth')) + 'px';
}
// Need to adjust our positioning to be relative to the appendTo container
// if it's not the body element
if (!appendToBody) {
var appendOffset = $position.offset(appendTo);
css.top = pos.top - appendOffset.top + 'px';
if (!rightalign) {
css.left = pos.left - appendOffset.left + 'px';
} else {
css.right = window.innerWidth -
(pos.left - appendOffset.left + $element.prop('offsetWidth')) + 'px';
}
}
self.dropdownMenu.css(css);
}
var openContainer = appendTo ? appendTo : $element;
var dropdownOpenClass = appendTo ? appendToOpenClass : openClass;
var hasOpenClass = openContainer.hasClass(dropdownOpenClass);
var isOnlyOpen = uibDropdownService.isOnlyOpen($scope, appendTo);
if (hasOpenClass === !isOpen) {
var toggleClass;
if (appendTo) {
toggleClass = !isOnlyOpen ? 'addClass' : 'removeClass';
} else {
toggleClass = isOpen ? 'addClass' : 'removeClass';
}
$animate[toggleClass](openContainer, dropdownOpenClass).then(function() {
if (angular.isDefined(isOpen) && isOpen !== wasOpen) {
toggleInvoker($scope, { open: !!isOpen });
}
});
}
if (isOpen) {
if (self.dropdownMenuTemplateUrl) {
$templateRequest(self.dropdownMenuTemplateUrl).then(function(tplContent) {
templateScope = scope.$new();
$compile(tplContent.trim())(templateScope, function(dropdownElement) {
var newEl = dropdownElement;
self.dropdownMenu.replaceWith(newEl);
self.dropdownMenu = newEl;
$document.on('keydown', uibDropdownService.keybindFilter);
});
});
} else {
$document.on('keydown', uibDropdownService.keybindFilter);
}
scope.focusToggleElement();
uibDropdownService.open(scope, $element, appendTo);
} else {
uibDropdownService.close(scope, $element, appendTo);
if (self.dropdownMenuTemplateUrl) {
if (templateScope) {
templateScope.$destroy();
}
var newEl = angular.element('');
self.dropdownMenu.replaceWith(newEl);
self.dropdownMenu = newEl;
}
self.selectedOption = null;
}
if (angular.isFunction(setIsOpen)) {
setIsOpen($scope, isOpen);
}
});
}])
.directive('uibDropdown', function() {
return {
controller: 'UibDropdownController',
link: function(scope, element, attrs, dropdownCtrl) {
dropdownCtrl.init();
}
};
})
.directive('uibDropdownMenu', function() {
return {
restrict: 'A',
require: '?^uibDropdown',
link: function(scope, element, attrs, dropdownCtrl) {
if (!dropdownCtrl || angular.isDefined(attrs.dropdownNested)) {
return;
}
element.addClass('dropdown-menu');
var tplUrl = attrs.templateUrl;
if (tplUrl) {
dropdownCtrl.dropdownMenuTemplateUrl = tplUrl;
}
if (!dropdownCtrl.dropdownMenu) {
dropdownCtrl.dropdownMenu = element;
}
}
};
})
.directive('uibDropdownToggle', function() {
return {
require: '?^uibDropdown',
link: function(scope, element, attrs, dropdownCtrl) {
if (!dropdownCtrl) {
return;
}
element.addClass('dropdown-toggle');
dropdownCtrl.toggleElement = element;
var toggleDropdown = function(event) {
event.preventDefault();
if (!element.hasClass('disabled') && !attrs.disabled) {
scope.$apply(function() {
dropdownCtrl.toggle();
});
}
};
element.on('click', toggleDropdown);
// WAI-ARIA
element.attr({ 'aria-haspopup': true, 'aria-expanded': false });
scope.$watch(dropdownCtrl.isOpen, function(isOpen) {
element.attr('aria-expanded', !!isOpen);
});
scope.$on('$destroy', function() {
element.off('click', toggleDropdown);
});
}
};
});
================================================
FILE: src/dropdown/index-nocss.js
================================================
require('../multiMap');
require('../position/index-nocss.js');
require('./dropdown');
var MODULE_NAME = 'ui.bootstrap.module.dropdown';
angular.module(MODULE_NAME, ['ui.bootstrap.dropdown']);
module.exports = MODULE_NAME;
================================================
FILE: src/dropdown/index.js
================================================
require('../position/position.css');
module.exports = require('./index-nocss.js');
================================================
FILE: src/dropdown/test/dropdown.spec.js
================================================
describe('uib-dropdown', function() {
var $animate, $compile, $rootScope, $document, $templateCache, dropdownConfig, element, $browser, $log;
beforeEach(module('ngAnimateMock'));
beforeEach(module('ui.bootstrap.dropdown'));
beforeEach(inject(function(_$animate_, _$compile_, _$rootScope_, _$document_, _$templateCache_, uibDropdownConfig, _$browser_, _$log_) {
$animate = _$animate_;
$compile = _$compile_;
$rootScope = _$rootScope_;
$document = _$document_;
$templateCache = _$templateCache_;
dropdownConfig = uibDropdownConfig;
$browser = _$browser_;
$log = _$log_;
}));
afterEach(function() {
element.remove();
});
var clickDropdownToggle = function(elm) {
elm = elm || element;
elm.find('a[uib-dropdown-toggle]').click();
};
var triggerKeyDown = function (element, keyCode) {
var e = $.Event('keydown');
spyOn(e, 'stopPropagation');
e.stopPropagation.and.callThrough();
e.which = keyCode;
element.trigger(e);
return e;
};
describe('basic', function() {
function dropdown() {
return $compile('
')($rootScope);
}
beforeEach(function() {
element = dropdown();
});
it('should toggle on `a` click', function() {
expect(element).not.toHaveClass(dropdownConfig.openClass);
clickDropdownToggle();
expect(element).toHaveClass(dropdownConfig.openClass);
clickDropdownToggle();
expect(element).not.toHaveClass(dropdownConfig.openClass);
});
it('should toggle when an option is clicked', function() {
$document.find('body').append(element);
expect(element).not.toHaveClass(dropdownConfig.openClass);
clickDropdownToggle();
expect(element).toHaveClass(dropdownConfig.openClass);
var optionEl = element.find('ul > li').eq(0).find('a').eq(0);
optionEl.click();
expect(element).not.toHaveClass(dropdownConfig.openClass);
});
it('should close on document click', function() {
clickDropdownToggle();
expect(element).toHaveClass(dropdownConfig.openClass);
$document.click();
expect(element).not.toHaveClass(dropdownConfig.openClass);
});
it('should close on escape key & focus toggle element', function() {
var dropdownMenu = element.find('[uib-dropdown-menu]');
$document.find('body').append(element);
clickDropdownToggle();
var event = triggerKeyDown(dropdownMenu, 27);
expect(element).not.toHaveClass(dropdownConfig.openClass);
expect(element.find('a')).toHaveFocus();
expect(event.stopPropagation).toHaveBeenCalled();
});
it('should not close on backspace key', function() {
clickDropdownToggle();
triggerKeyDown(element, 8);
expect(element).toHaveClass(dropdownConfig.openClass);
});
it('should not close on right click', function() {
clickDropdownToggle();
element.find('ul a').trigger({
type: 'mousedown',
which: 3
});
expect(element).toHaveClass(dropdownConfig.openClass);
});
it('should only allow one dropdown to be open at once', function() {
var elm1 = dropdown();
var elm2 = dropdown();
expect(elm1).not.toHaveClass(dropdownConfig.openClass);
expect(elm2).not.toHaveClass(dropdownConfig.openClass);
clickDropdownToggle(elm1);
expect(elm1).toHaveClass(dropdownConfig.openClass);
expect(elm2).not.toHaveClass(dropdownConfig.openClass);
clickDropdownToggle(elm2);
expect(elm1).not.toHaveClass(dropdownConfig.openClass);
expect(elm2).toHaveClass(dropdownConfig.openClass);
});
it('should not toggle if the element has `disabled` class', function() {
var elm = $compile('
')($rootScope);
clickDropdownToggle( elm );
expect(elm).not.toHaveClass(dropdownConfig.openClass);
});
it('should not toggle if the element is disabled', function() {
var elm = $compile('
')($rootScope);
elm.find('button').click();
expect(elm).not.toHaveClass(dropdownConfig.openClass);
});
it('should not toggle if the element has `ng-disabled` as true', function() {
$rootScope.isdisabled = true;
var elm = $compile('
')($rootScope);
$rootScope.$digest();
elm.find('div').click();
expect(elm).not.toHaveClass(dropdownConfig.openClass);
$rootScope.isdisabled = false;
$rootScope.$digest();
elm.find('div').click();
expect(elm).toHaveClass(dropdownConfig.openClass);
});
it('should unbind events on scope destroy', function() {
var $scope = $rootScope.$new();
var elm = $compile('
')($scope);
$scope.$digest();
var buttonEl = elm.find('button');
buttonEl.click();
expect(elm).toHaveClass(dropdownConfig.openClass);
buttonEl.click();
expect(elm).not.toHaveClass(dropdownConfig.openClass);
$scope.$destroy();
buttonEl.click();
expect(elm).not.toHaveClass(dropdownConfig.openClass);
});
// issue 270
it('executes other document click events normally', function() {
var checkboxEl = $compile('
')($rootScope);
$rootScope.$digest();
expect(element).not.toHaveClass(dropdownConfig.openClass);
expect($rootScope.clicked).toBeFalsy();
clickDropdownToggle();
expect(element).toHaveClass(dropdownConfig.openClass);
expect($rootScope.clicked).toBeFalsy();
checkboxEl.click();
expect($rootScope.clicked).toBeTruthy();
});
// WAI-ARIA
it('should aria markup to the `dropdown-toggle`', function() {
var toggleEl = element.find('a');
expect(toggleEl.attr('aria-haspopup')).toBe('true');
expect(toggleEl.attr('aria-expanded')).toBe('false');
clickDropdownToggle();
expect(toggleEl.attr('aria-expanded')).toBe('true');
clickDropdownToggle();
expect(toggleEl.attr('aria-expanded')).toBe('false');
});
// pr/issue 3274
it('should not raise $digest:inprog if dismissed during a digest cycle', function() {
clickDropdownToggle();
expect(element).toHaveClass(dropdownConfig.openClass);
$rootScope.$apply(function() {
$document.click();
});
expect(element).not.toHaveClass(dropdownConfig.openClass);
});
});
describe('using dropdownMenuTemplate', function() {
function dropdown() {
$templateCache.put('custom.html', '');
return $compile('
')($rootScope);
}
beforeEach(function() {
element = dropdown();
});
it('should apply custom template for dropdown menu', function() {
element.find('a').click();
expect(element.find('ul.uib-dropdown-menu').eq(0).find('li').eq(0).text()).toEqual('Item 1');
});
it('should clear ul when dropdown menu is closed', function() {
element.find('a').click();
expect(element.find('ul.uib-dropdown-menu').eq(0).find('li').eq(0).text()).toEqual('Item 1');
element.find('a').click();
expect(element.find('ul.uib-dropdown-menu').eq(0).find('li').length).toEqual(0);
});
});
describe('using dropdown-append-to-body', function() {
describe('with no value', function() {
function dropdown() {
return $compile('
')($rootScope);
}
beforeEach(function() {
element = dropdown();
$document.find('body').append(element);
});
afterEach(function() {
element.remove();
});
it('does not add the menu to the body', function() {
expect($document.find('#dropdown-menu').parent()[0]).not.toBe($document.find('body')[0]);
});
describe('when toggled open', function() {
var toggle;
beforeEach(function() {
toggle = element.find('[uib-dropdown-toggle]');
toggle.trigger('click');
});
it('adds the menu to the body', function() {
expect($document.find('#dropdown-menu').parent()[0]).toBe($document.find('body')[0]);
});
describe('when toggled closed', function() {
beforeEach(function() {
toggle.trigger('click');
});
it('removes the menu from body', function() {
expect($document.find('#dropdown-menu').parent()[0]).not.toBe($document.find('body')[0]);
});
});
describe('when closed by clicking on menu', function() {
var menu;
beforeEach(function() {
menu = $document.find('#dropdown-menu a');
menu.focus();
menu.trigger('click');
});
it('focuses the dropdown element on close', function() {
expect(document.activeElement).toBe(toggle[0]);
});
it('removes the menu from body', function() {
expect($document.find('#dropdown-menu').parent()[0]).not.toBe($document.find('body')[0]);
});
});
describe('when the dropdown is removed', function() {
beforeEach(function() {
element.remove();
$rootScope.$digest();
});
it('removes the menu from body', function() {
expect($document.find('#dropdown-menu').parent()[0]).not.toBe($document.find('body')[0]);
});
});
});
});
describe('with a value', function() {
function dropdown() {
return $compile('
')($rootScope);
}
describe('that is not false', function() {
beforeEach(function() {
$rootScope.appendToBody = 'sure';
element = dropdown();
$document.find('body').append(element);
});
afterEach(function() {
element.remove();
});
it('does not add the menu to the body', function() {
expect($document.find('#dropdown-menu').parent()[0]).not.toBe($document.find('body')[0]);
});
describe('when toggled open', function() {
var toggle;
beforeEach(function() {
toggle = element.find('[uib-dropdown-toggle]');
toggle.trigger('click');
});
it('adds the menu to the body', function() {
expect($document.find('#dropdown-menu').parent()[0]).toBe($document.find('body')[0]);
});
describe('when toggled closed', function() {
beforeEach(function() {
toggle.trigger('click');
});
it('removes the menu from body', function() {
expect($document.find('#dropdown-menu').parent()[0]).not.toBe($document.find('body')[0]);
});
});
describe('when closed by clicking on menu', function() {
var menu;
beforeEach(function() {
menu = $document.find('#dropdown-menu a');
menu.focus();
menu.trigger('click');
});
it('focuses the dropdown element on close', function() {
expect(document.activeElement).toBe(toggle[0]);
});
it('removes the menu from body', function() {
expect($document.find('#dropdown-menu').parent()[0]).not.toBe($document.find('body')[0]);
});
});
describe('when the dropdown is removed', function() {
beforeEach(function() {
element.remove();
$rootScope.$digest();
});
it('removes the menu from body', function() {
expect($document.find('#dropdown-menu').parent()[0]).not.toBe($document.find('body')[0]);
});
});
});
});
describe('that is false', function() {
beforeEach(function() {
$rootScope.appendToBody = false;
element = dropdown();
$document.find('body').append(element);
});
afterEach(function() {
element.remove();
});
it('does not add the menu to the body', function() {
expect($document.find('#dropdown-menu').parent()[0]).not.toBe($document.find('body')[0]);
});
describe('when toggled open', function() {
var toggle;
beforeEach(function() {
toggle = element.find('[uib-dropdown-toggle]');
toggle.trigger('click');
});
it('does not add the menu to the body', function() {
expect($document.find('#dropdown-menu').parent()[0]).not.toBe($document.find('body')[0]);
});
describe('when toggled closed', function() {
beforeEach(function() {
toggle.trigger('click');
});
it('does not remove the menu', function() {
expect($document.find('#dropdown-menu').length).not.toEqual(0);
});
});
describe('when closed by clicking on menu', function() {
var menu;
beforeEach(function() {
menu = $document.find('#dropdown-menu a');
menu.focus();
menu.trigger('click');
});
it('focuses the dropdown element on close', function() {
expect(document.activeElement).toBe(toggle[0]);
});
it('does not removes the menu from body', function() {
expect($document.find('#dropdown-menu').parent()[0]).not.toBe($document.find('body')[0]);
});
});
describe('when the dropdown is removed', function() {
beforeEach(function() {
element.remove();
$rootScope.$digest();
});
it('removes the menu from body', function() {
expect($document.find('#dropdown-menu').parent()[0]).not.toBe($document.find('body')[0]);
});
});
});
});
});
});
describe('using dropdown-append-to', function() {
var initialPage, container;
function dropdown() {
return $compile('
')($rootScope);
}
beforeEach(function() {
$document.find('body').append(angular.element('
'));
$rootScope.appendTo = container = $document.find('#dropdown-container');
element = dropdown();
$document.find('body').append(element);
});
afterEach(function() {
// Cleanup the extra elements we appended
$document.find('#dropdown-container').remove();
});
it('does not add the menu to the container', function() {
expect($document.find('#dropdown-menu').parent()[0]).not.toBe(container[0]);
});
it('does not add open class on container', function() {
expect(container).not.toHaveClass('uib-dropdown-open');
});
describe('when toggled open', function() {
var toggle;
beforeEach(function() {
toggle = element.find('[uib-dropdown-toggle]');
toggle.trigger('click');
});
it('adds the menu to the container', function() {
expect($document.find('#dropdown-menu').parent()[0]).toBe(container[0]);
});
it('adds open class on container', function() {
expect(container).toHaveClass('uib-dropdown-open');
});
describe('when toggled closed', function() {
beforeEach(function() {
toggle.trigger('click');
});
it('removes the menu from the container', function() {
expect($document.find('#dropdown-menu').parent()[0]).not.toBe($document.find('body')[0]);
});
it('removes open class from container', function() {
expect(container).not.toHaveClass('uib-dropdown-open');
});
});
describe('when closed by clicking on menu', function() {
var menu;
beforeEach(function() {
menu = $document.find('#dropdown-menu a');
menu.focus();
menu.trigger('click');
});
it('focuses the dropdown element on close', function() {
expect(document.activeElement).toBe(toggle[0]);
});
it('removes the menu from the container', function() {
expect($document.find('#dropdown-menu').parent()[0]).not.toBe($document.find('body')[0]);
});
it('removes open class from container', function() {
expect(container).not.toHaveClass('uib-dropdown-open');
});
});
describe('when the dropdown is removed', function() {
beforeEach(function() {
element.remove();
$rootScope.$digest();
});
it('removes the menu from the container', function() {
expect($document.find('#dropdown-menu').parent()[0]).not.toBe($document.find('body')[0]);
});
});
});
});
describe('using dropdown-append-to with two dropdowns', function() {
function dropdown() {
return $compile('
')($rootScope);
}
beforeEach(function() {
$document.find('body').append(angular.element('
'));
$rootScope.appendTo = $document.find('#dropdown-container');
$rootScope.log = jasmine.createSpy('log');
element = dropdown();
$document.find('body').append(element);
});
afterEach(function() {
// Cleanup the extra elements we appended
$document.find('#dropdown-container').remove();
});
it('should keep the class when toggling from one dropdown to another with the same container', function() {
var container = $document.find('#dropdown-container');
expect(container).not.toHaveClass('uib-dropdown-open');
element.find('.dropdown1 [uib-dropdown-toggle]').click();
expect(container).toHaveClass('uib-dropdown-open');
element.find('.dropdown2 [uib-dropdown-toggle]').click();
expect(container).toHaveClass('uib-dropdown-open');
});
});
describe('using is-open', function() {
describe('with uib-dropdown-toggle', function() {
beforeEach(function() {
$rootScope.isopen = true;
element = $compile('
')($rootScope);
$rootScope.$digest();
});
it('should be open initially', function() {
expect(element).toHaveClass(dropdownConfig.openClass);
});
it('should change `is-open` binding when toggles', function() {
clickDropdownToggle();
expect($rootScope.isopen).toBe(false);
});
it('should toggle when `is-open` changes', function() {
$rootScope.isopen = false;
$rootScope.$digest();
expect(element).not.toHaveClass(dropdownConfig.openClass);
});
it('focus toggle element when opening', function() {
$document.find('body').append(element);
clickDropdownToggle();
$rootScope.isopen = false;
$rootScope.$digest();
expect(element.find('a')).not.toHaveFocus();
$rootScope.isopen = true;
$rootScope.$digest();
expect(element.find('a')).toHaveFocus();
});
});
describe('without uib-dropdown-toggle', function() {
beforeEach(function() {
$rootScope.isopen = true;
element = $compile('
')($rootScope);
$rootScope.$digest();
});
it('should be open initially', function() {
expect(element).toHaveClass(dropdownConfig.openClass);
});
it('should toggle when `is-open` changes', function() {
$rootScope.isopen = false;
$rootScope.$digest();
expect(element).not.toHaveClass(dropdownConfig.openClass);
});
});
});
describe('using on-toggle', function() {
describe('with is-open to false', function() {
beforeEach(function() {
$rootScope.toggleHandler = jasmine.createSpy('toggleHandler');
$rootScope.isopen = false;
element = $compile('
')($rootScope);
$rootScope.$digest();
});
it('should not have been called initially', function() {
expect($rootScope.toggleHandler).not.toHaveBeenCalled();
});
it('should call it correctly when toggles', function() {
$rootScope.isopen = true;
$rootScope.$digest();
$animate.flush();
$rootScope.$digest();
expect($rootScope.toggleHandler).toHaveBeenCalledWith(true);
clickDropdownToggle();
$animate.flush();
$rootScope.$digest();
expect($rootScope.toggleHandler).toHaveBeenCalledWith(false);
});
});
describe('with is-open to true', function() {
beforeEach(function() {
$rootScope.toggleHandler = jasmine.createSpy('toggleHandler');
$rootScope.isopen = true;
element = $compile('
')($rootScope);
$rootScope.$digest();
});
it('should not have been called initially', function() {
expect($rootScope.toggleHandler).not.toHaveBeenCalled();
});
it('should call it correctly when toggles', function() {
$rootScope.isopen = false;
$rootScope.$digest();
$animate.flush();
$rootScope.$digest();
expect($rootScope.toggleHandler).toHaveBeenCalledWith(false);
$rootScope.isopen = true;
$rootScope.$digest();
$animate.flush();
$rootScope.$digest();
expect($rootScope.toggleHandler).toHaveBeenCalledWith(true);
});
});
describe('without is-open', function() {
beforeEach(function() {
$rootScope.toggleHandler = jasmine.createSpy('toggleHandler');
element = $compile('
')($rootScope);
$rootScope.$digest();
});
it('should not have been called initially', function() {
expect($rootScope.toggleHandler).not.toHaveBeenCalled();
});
it('should call it when clicked', function() {
clickDropdownToggle();
$animate.flush();
$rootScope.$digest();
expect($rootScope.toggleHandler).toHaveBeenCalledWith(true);
clickDropdownToggle();
$animate.flush();
$rootScope.$digest();
expect($rootScope.toggleHandler).toHaveBeenCalledWith(false);
});
});
});
describe('using auto-close', function() {
function dropdown(autoClose) {
return $compile('
')($rootScope);
}
describe('always', function() {
it('should close on document click if no auto-close is specified', function() {
element = dropdown();
clickDropdownToggle();
expect(element).toHaveClass(dropdownConfig.openClass);
$document.click();
expect(element).not.toHaveClass(dropdownConfig.openClass);
});
it('should close on document click if empty auto-close is specified', function() {
element = dropdown('');
clickDropdownToggle();
expect(element).toHaveClass(dropdownConfig.openClass);
$document.click();
expect(element).not.toHaveClass(dropdownConfig.openClass);
});
});
describe('disabled', function() {
it('auto-close="disabled"', function() {
element = dropdown('disabled');
clickDropdownToggle();
expect(element).toHaveClass(dropdownConfig.openClass);
$document.click();
expect(element).toHaveClass(dropdownConfig.openClass);
});
it('control with is-open', function() {
$rootScope.isopen = true;
element = $compile('
')($rootScope);
$rootScope.$digest();
expect(element).toHaveClass(dropdownConfig.openClass);
//should remain open
$document.click();
expect(element).toHaveClass(dropdownConfig.openClass);
//now should close
$rootScope.isopen = false;
$rootScope.$digest();
expect(element).not.toHaveClass(dropdownConfig.openClass);
});
it('should close anyway if toggle is clicked', function() {
element = dropdown('disabled');
clickDropdownToggle();
expect(element).toHaveClass(dropdownConfig.openClass);
clickDropdownToggle();
expect(element).not.toHaveClass(dropdownConfig.openClass);
});
it('should close anyway if esc is pressed', function() {
element = dropdown('disabled');
var dropdownMenu = element.find('[uib-dropdown-menu]');
$document.find('body').append(element);
clickDropdownToggle();
triggerKeyDown(dropdownMenu, 27);
expect(element).not.toHaveClass(dropdownConfig.openClass);
expect(element.find('a')).toHaveFocus();
});
it('should close anyway if another dropdown is opened', function() {
var elm1 = dropdown('disabled');
var elm2 = dropdown();
expect(elm1).not.toHaveClass(dropdownConfig.openClass);
expect(elm2).not.toHaveClass(dropdownConfig.openClass);
clickDropdownToggle(elm1);
expect(elm1).toHaveClass(dropdownConfig.openClass);
expect(elm2).not.toHaveClass(dropdownConfig.openClass);
clickDropdownToggle(elm2);
expect(elm1).not.toHaveClass(dropdownConfig.openClass);
expect(elm2).toHaveClass(dropdownConfig.openClass);
});
});
describe('outsideClick', function() {
it('should close only on a click outside of the dropdown menu', function() {
element = dropdown('outsideClick');
clickDropdownToggle();
expect(element).toHaveClass(dropdownConfig.openClass);
element.find('ul li a').click();
expect(element).toHaveClass(dropdownConfig.openClass);
$document.click();
expect(element).not.toHaveClass(dropdownConfig.openClass);
});
it('should work with dropdown-append-to-body', function() {
element = $compile('
')($rootScope);
clickDropdownToggle();
var dropdownMenu = $document.find('#dropdown-menu');
expect(dropdownMenu.parent()).toHaveClass(dropdownConfig.appendToOpenClass);
dropdownMenu.find('li').eq(0).trigger('click');
expect(dropdownMenu.parent()).toHaveClass(dropdownConfig.appendToOpenClass);
$document.click();
expect(dropdownMenu.parent()).not.toHaveClass(dropdownConfig.appendToOpenClass);
});
});
});
describe('using keyboard-nav', function() {
function dropdown() {
return $compile('
')($rootScope);
}
function getFocusedElement() {
return angular.element(document.activeElement);
}
beforeEach(function() {
element = dropdown();
});
it('should focus first list element when down arrow pressed', function() {
$document.find('body').append(element);
clickDropdownToggle();
triggerKeyDown(getFocusedElement(), 40);
expect(element).toHaveClass(dropdownConfig.openClass);
var optionEl = element.find('ul').eq(0).find('a').eq(0);
expect(optionEl).toHaveFocus();
});
it('should not focus first list element when down arrow pressed if closed', function() {
$document.find('body').append(element);
triggerKeyDown(getFocusedElement(), 40);
expect(element).not.toHaveClass(dropdownConfig.openClass);
var focusEl = element.find('ul').eq(0).find('a').eq(0);
expect(focusEl).not.toHaveFocus();
});
it('should focus second list element when down arrow pressed twice', function() {
$document.find('body').append(element);
clickDropdownToggle();
triggerKeyDown(getFocusedElement(), 40);
triggerKeyDown(getFocusedElement(), 40);
expect(element).toHaveClass(dropdownConfig.openClass);
var focusEl = element.find('ul').eq(0).find('a').eq(1);
expect(focusEl).toHaveFocus();
});
it('should not focus first list element when up arrow pressed after dropdown toggled', function() {
$document.find('body').append(element);
clickDropdownToggle();
expect(element).toHaveClass(dropdownConfig.openClass);
triggerKeyDown(getFocusedElement(), 38);
var focusEl = element.find('ul').eq(0).find('a').eq(0);
expect(focusEl).not.toHaveFocus();
});
it('should focus last list element when up arrow pressed after dropdown toggled', function() {
$document.find('body').append(element);
clickDropdownToggle();
triggerKeyDown(getFocusedElement(), 38);
expect(element).toHaveClass(dropdownConfig.openClass);
var focusEl = element.find('ul').eq(0).find('a').eq(1);
expect(focusEl).toHaveFocus();
});
it('should not change focus when other keys are pressed', function() {
$document.find('body').append(element);
clickDropdownToggle();
triggerKeyDown(getFocusedElement(), 37);
expect(element).toHaveClass(dropdownConfig.openClass);
var focusEl = element.find('ul').eq(0).find('a');
expect(focusEl[0]).not.toHaveFocus();
expect(focusEl[1]).not.toHaveFocus();
});
it('should focus first list element when down arrow pressed 2x and up pressed 1x', function() {
$document.find('body').append(element);
clickDropdownToggle();
triggerKeyDown(getFocusedElement(), 40);
triggerKeyDown(getFocusedElement(), 40);
triggerKeyDown(getFocusedElement(), 38);
expect(element).toHaveClass(dropdownConfig.openClass);
var focusEl = element.find('ul').eq(0).find('a').eq(0);
expect(focusEl).toHaveFocus();
});
it('should stay focused on final list element if down pressed at list end', function() {
$document.find('body').append(element);
clickDropdownToggle();
triggerKeyDown(getFocusedElement(), 40);
triggerKeyDown(getFocusedElement(), 40);
expect(element).toHaveClass(dropdownConfig.openClass);
var focusEl = element.find('ul').eq(0).find('a').eq(1);
expect(focusEl).toHaveFocus();
triggerKeyDown(element, 40);
expect(focusEl).toHaveFocus();
});
it('should close if esc is pressed while focused', function() {
element = dropdown('disabled');
$document.find('body').append(element);
clickDropdownToggle();
triggerKeyDown(getFocusedElement(), 40);
expect(element).toHaveClass(dropdownConfig.openClass);
var focusEl = element.find('ul').eq(0).find('a').eq(0);
expect(focusEl).toHaveFocus();
triggerKeyDown(getFocusedElement(), 27);
expect(element).not.toHaveClass(dropdownConfig.openClass);
});
describe('with dropdown-append-to-body', function() {
function dropdown() {
return $compile('
foo')($rootScope);
}
beforeEach(function() {
element = dropdown();
});
it('should focus first list element when down arrow pressed', function() {
$document.find('body').append(element);
clickDropdownToggle();
var dropdownMenu = $document.find('#dropdown-menu');
triggerKeyDown(getFocusedElement(), 40);
expect(dropdownMenu.parent()).toHaveClass(dropdownConfig.appendToOpenClass);
var focusEl = $document.find('ul').eq(0).find('a');
expect(focusEl).toHaveFocus();
});
it('should focus second list element when down arrow pressed twice', function() {
$document.find('body').append(element);
clickDropdownToggle();
var dropdownMenu = $document.find('#dropdown-menu');
triggerKeyDown(getFocusedElement(), 40);
triggerKeyDown(getFocusedElement(), 40);
triggerKeyDown(getFocusedElement(), 40);
expect(dropdownMenu.parent()).toHaveClass(dropdownConfig.appendToOpenClass);
var elem1 = $document.find('ul');
var elem2 = elem1.find('a');
var focusEl = $document.find('ul').eq(0).find('a').eq(1);
expect(focusEl).toHaveFocus();
});
});
});
// issue #5942
describe('using dropdown-append-to-body with dropdown-menu-right class', function() {
function dropdown() {
return $compile('
Toggle menu')($rootScope);
}
beforeEach(function() {
element = dropdown();
$document.find('body').append(element);
var menu = $document.find('#dropdown-menu');
menu.css('position', 'absolute');
});
afterEach(function() {
element.remove();
});
it('should align the menu correctly when the body has no vertical scrollbar', function() {
var toggle = element.find('[uib-dropdown-toggle]');
var menu = $document.find('#dropdown-menu');
toggle.trigger('click');
// Get the offsets of the rightmost position of both the toggle and the menu (offset from the left of the window)
var toggleRight = Math.round(toggle.offset().left + toggle.outerWidth());
var menuRight = Math.round(menu.offset().left + menu.outerWidth());
expect(menuRight).toBe(toggleRight);
});
});
});
================================================
FILE: src/isClass/index.js
================================================
require('./isClass');
var MODULE_NAME = 'ui.bootstrap.module.isClass';
angular.module(MODULE_NAME, ['ui.bootstrap.isClass']);
module.exports = MODULE_NAME;
================================================
FILE: src/isClass/isClass.js
================================================
// Avoiding use of ng-class as it creates a lot of watchers when a class is to be applied to
// at most one element.
angular.module('ui.bootstrap.isClass', [])
.directive('uibIsClass', [
'$animate',
function ($animate) {
// 11111111 22222222
var ON_REGEXP = /^\s*([\s\S]+?)\s+on\s+([\s\S]+?)\s*$/;
// 11111111 22222222
var IS_REGEXP = /^\s*([\s\S]+?)\s+for\s+([\s\S]+?)\s*$/;
var dataPerTracked = {};
return {
restrict: 'A',
compile: function(tElement, tAttrs) {
var linkedScopes = [];
var instances = [];
var expToData = {};
var lastActivated = null;
var onExpMatches = tAttrs.uibIsClass.match(ON_REGEXP);
var onExp = onExpMatches[2];
var expsStr = onExpMatches[1];
var exps = expsStr.split(',');
return linkFn;
function linkFn(scope, element, attrs) {
linkedScopes.push(scope);
instances.push({
scope: scope,
element: element
});
exps.forEach(function(exp, k) {
addForExp(exp, scope);
});
scope.$on('$destroy', removeScope);
}
function addForExp(exp, scope) {
var matches = exp.match(IS_REGEXP);
var clazz = scope.$eval(matches[1]);
var compareWithExp = matches[2];
var data = expToData[exp];
if (!data) {
var watchFn = function(compareWithVal) {
var newActivated = null;
instances.some(function(instance) {
var thisVal = instance.scope.$eval(onExp);
if (thisVal === compareWithVal) {
newActivated = instance;
return true;
}
});
if (data.lastActivated !== newActivated) {
if (data.lastActivated) {
$animate.removeClass(data.lastActivated.element, clazz);
}
if (newActivated) {
$animate.addClass(newActivated.element, clazz);
}
data.lastActivated = newActivated;
}
};
expToData[exp] = data = {
lastActivated: null,
scope: scope,
watchFn: watchFn,
compareWithExp: compareWithExp,
watcher: scope.$watch(compareWithExp, watchFn)
};
}
data.watchFn(scope.$eval(compareWithExp));
}
function removeScope(e) {
var removedScope = e.targetScope;
var index = linkedScopes.indexOf(removedScope);
linkedScopes.splice(index, 1);
instances.splice(index, 1);
if (linkedScopes.length) {
var newWatchScope = linkedScopes[0];
angular.forEach(expToData, function(data) {
if (data.scope === removedScope) {
data.watcher = newWatchScope.$watch(data.compareWithExp, data.watchFn);
data.scope = newWatchScope;
}
});
} else {
expToData = {};
}
}
}
};
}]);
================================================
FILE: src/isClass/test/isClass.spec.js
================================================
describe('uibIsClass', function() {
var $rootScope;
beforeEach(module('ui.bootstrap.isClass'));
beforeEach(inject(function($compile, _$rootScope_) {
$rootScope = _$rootScope_;
$rootScope.activeClass = 'active';
$rootScope.items = [1, 2, 3];
element = $compile('
')($rootScope);
$rootScope.$digest();
}));
it('initializes classes correctly', function() {
expect(element.find('.active').length).toEqual(0);
});
it('sets classes correctly', function() {
$rootScope.activeItem = 2;
$rootScope.$digest();
expect(element.find('.active').text()).toEqual('2');
$rootScope.items.splice(1, 1);
$rootScope.$digest();
expect(element.find('.active').length).toEqual(0);
});
it('handles removal of items correctly', function() {
$rootScope.activeItem = 2;
$rootScope.$digest();
expect(element.find('.active').text()).toEqual('2');
$rootScope.items.splice(1, 1);
$rootScope.$digest();
expect(element.find('.active').length).toEqual(0);
$rootScope.activeItem = 1;
$rootScope.$digest();
expect(element.find('.active').text()).toEqual('1');
});
it('handles moving of items', function() {
$rootScope.activeItem = 2;
$rootScope.items = [2, 1, 3];
$rootScope.$digest();
expect(element.find('.active').text()).toEqual('2');
expect(element.find('.active').length).toEqual(1);
expect(element.find('.active').index()).toEqual(0);
$rootScope.items = [4, 3, 2];
$rootScope.$digest();
expect(element.find('.active').text()).toEqual('2');
expect(element.find('.active').length).toEqual(1);
expect(element.find('.active').index()).toEqual(2);
});
it('handles emptying and re-adding the items', function() {
$rootScope.activeItem = 2;
$rootScope.items = [];
$rootScope.$digest();
expect(element.find('.active').length).toEqual(0);
$rootScope.items = [4, 3, 2];
$rootScope.$digest();
expect(element.find('.active').text()).toEqual('2');
expect(element.find('.active').index()).toEqual(2);
});
it('handles undefined items', function() {
$rootScope.activeItem = undefined;
$rootScope.items = [];
$rootScope.$digest();
expect(element.find('.active').length).toEqual(0);
$rootScope.items = [4, 3, undefined];
$rootScope.$digest();
expect(element.find('.active').length).toEqual(1);
expect(element.find('.active').text()).toEqual('');
});
});
================================================
FILE: src/modal/docs/demo.html
================================================
Selection from a modal: {{ $ctrl.selected }}
================================================
FILE: src/modal/docs/demo.js
================================================
angular.module('ui.bootstrap.demo').controller('ModalDemoCtrl', function ($uibModal, $log, $document) {
var $ctrl = this;
$ctrl.items = ['item1', 'item2', 'item3'];
$ctrl.animationsEnabled = true;
$ctrl.open = function (size, parentSelector) {
var parentElem = parentSelector ?
angular.element($document[0].querySelector('.modal-demo ' + parentSelector)) : undefined;
var modalInstance = $uibModal.open({
animation: $ctrl.animationsEnabled,
ariaLabelledBy: 'modal-title',
ariaDescribedBy: 'modal-body',
templateUrl: 'myModalContent.html',
controller: 'ModalInstanceCtrl',
controllerAs: '$ctrl',
size: size,
appendTo: parentElem,
resolve: {
items: function () {
return $ctrl.items;
}
}
});
modalInstance.result.then(function (selectedItem) {
$ctrl.selected = selectedItem;
}, function () {
$log.info('Modal dismissed at: ' + new Date());
});
};
$ctrl.openComponentModal = function () {
var modalInstance = $uibModal.open({
animation: $ctrl.animationsEnabled,
component: 'modalComponent',
resolve: {
items: function () {
return $ctrl.items;
}
}
});
modalInstance.result.then(function (selectedItem) {
$ctrl.selected = selectedItem;
}, function () {
$log.info('modal-component dismissed at: ' + new Date());
});
};
$ctrl.openMultipleModals = function () {
$uibModal.open({
animation: $ctrl.animationsEnabled,
ariaLabelledBy: 'modal-title-bottom',
ariaDescribedBy: 'modal-body-bottom',
templateUrl: 'stackedModal.html',
size: 'sm',
controller: function($scope) {
$scope.name = 'bottom';
}
});
$uibModal.open({
animation: $ctrl.animationsEnabled,
ariaLabelledBy: 'modal-title-top',
ariaDescribedBy: 'modal-body-top',
templateUrl: 'stackedModal.html',
size: 'sm',
controller: function($scope) {
$scope.name = 'top';
}
});
};
$ctrl.toggleAnimation = function () {
$ctrl.animationsEnabled = !$ctrl.animationsEnabled;
};
});
// Please note that $uibModalInstance represents a modal window (instance) dependency.
// It is not the same as the $uibModal service used above.
angular.module('ui.bootstrap.demo').controller('ModalInstanceCtrl', function ($uibModalInstance, items) {
var $ctrl = this;
$ctrl.items = items;
$ctrl.selected = {
item: $ctrl.items[0]
};
$ctrl.ok = function () {
$uibModalInstance.close($ctrl.selected.item);
};
$ctrl.cancel = function () {
$uibModalInstance.dismiss('cancel');
};
});
// Please note that the close and dismiss bindings are from $uibModalInstance.
angular.module('ui.bootstrap.demo').component('modalComponent', {
templateUrl: 'myModalContent.html',
bindings: {
resolve: '<',
close: '&',
dismiss: '&'
},
controller: function () {
var $ctrl = this;
$ctrl.$onInit = function () {
$ctrl.items = $ctrl.resolve.items;
$ctrl.selected = {
item: $ctrl.items[0]
};
};
$ctrl.ok = function () {
$ctrl.close({$value: $ctrl.selected.item});
};
$ctrl.cancel = function () {
$ctrl.dismiss({$value: 'cancel'});
};
}
});
================================================
FILE: src/modal/docs/readme.md
================================================
`$uibModal` is a service to create modal windows.
Creating modals is straightforward: create a template and controller, and reference them when using `$uibModal`.
The `$uibModal` service has only one method: `open(options)`.
### $uibModal's open function
#### options parameter
* `animation`
_(Type: `boolean`, Default: `true`)_ -
Set to false to disable animations on new modal/backdrop. Does not toggle animations for modals/backdrops that are already displayed.
* `appendTo`
_(Type: `angular.element`, Default: `body`: Example: `$document.find('aside').eq(0)`)_ -
Appends the modal to a specific element.
* `ariaDescribedBy`
_(Type: `string`, `my-modal-description`)_ -
Sets the [`aria-describedby`](https://www.w3.org/TR/wai-aria/states_and_properties#aria-describedby) property on the modal. The value should be an id (without the leading `#`) pointing to the element that describes your modal. Typically, this will be the text on your modal, but does not include something the user would interact with, like buttons or a form. Omitting this option will not impact sighted users but will weaken your accessibility support.
* `ariaLabelledBy`
_(Type: `string`, `my-modal-title`)_ -
Sets the [`aria-labelledby`](https://www.w3.org/TR/wai-aria/states_and_properties#aria-labelledby) property on the modal. The value should be an id (without the leading `#`) pointing to the element that labels your modal. Typically, this will be a header element. Omitting this option will not impact sighted users but will weaken your accessibility support.
* `backdrop`
_(Type: `boolean|string`, Default: `true`)_ -
Controls presence of a backdrop. Allowed values: `true` (default), `false` (no backdrop), `'static'` (disables modal closing by click on the backdrop).
* `backdropClass`
_(Type: `string`)_ -
Additional CSS class(es) to be added to a modal backdrop template.
* `bindToController`
_(Type: `boolean`, Default: `false`)_ -
When used with `controllerAs` & set to `true`, it will bind the $scope properties onto the controller.
* `component`
_(Type: `string`, Example: `myComponent`)_ -
A string reference to the component to be rendered that is registered with Angular's compiler. If using a directive, the directive must have `restrict: 'E'` and a template or templateUrl set.
It supports these bindings:
* `close` - A method that can be used to close a modal, passing a result. The result must be passed in this format: `{$value: myResult}`
* `dismiss` - A method that can be used to dismiss a modal, passing a result. The result must be passed in this format: `{$value: myRejectedResult}`
* `modalInstance` - The modal instance. This is the same `$uibModalInstance` injectable found when using `controller`.
* `resolve` - An object of the modal resolve values. See [UI Router resolves](#ui-router-resolves) for details.
* `controller`
_(Type: `function|string|array`, Example: `MyModalController`)_ -
A controller for the modal instance, either a controller name as a string, or an inline controller function, optionally wrapped in array notation for dependency injection. Allows the controller-as syntax. Has a special `$uibModalInstance` injectable to access the modal instance.
* `controllerAs`
_(Type: `string`, Example: `ctrl`)_ -
An alternative to the controller-as syntax. Requires the `controller` option to be provided as well.
* `keyboard` -
_(Type: `boolean`, Default: `true`)_ -
Indicates whether the dialog should be closable by hitting the ESC key.
* `openedClass`
_(Type: `string`, Default: `modal-open`)_ -
Class added to the `body` element when the modal is opened.
* `resolve`
_(Type: `Object`)_ -
Members that will be resolved and passed to the controller as locals; it is equivalent of the `resolve` property in the router.
* `scope`
_(Type: `$scope`)_ -
The parent scope instance to be used for the modal's content. Defaults to `$rootScope`.
* `size`
_(Type: `string`, Example: `lg`)_ -
Optional suffix of modal window class. The value used is appended to the `modal-` class, i.e. a value of `sm` gives `modal-sm`.
* `template`
_(Type: `string`)_ -
Inline template representing the modal's content.
* `templateUrl`
_(Type: `string`)_ -
A path to a template representing modal's content. You need either a `template` or `templateUrl`.
* `windowClass`
_(Type: `string`)_ -
Additional CSS class(es) to be added to a modal window template.
* `windowTemplateUrl`
_(Type: `string`, Default: `uib/template/modal/window.html`)_ -
A path to a template overriding modal's window template.
* `windowTopClass`
_(Type: `string`)_ -
CSS class(es) to be added to the top modal window.
Global defaults may be set for `$uibModal` via `$uibModalProvider.options`.
#### return
The `open` method returns a modal instance, an object with the following properties:
* `close(result)`
_(Type: `function`)_ -
Can be used to close a modal, passing a result.
* `dismiss(reason)`
_(Type: `function`)_ -
Can be used to dismiss a modal, passing a reason.
* `result`
_(Type: `promise`)_ -
Is resolved when a modal is closed and rejected when a modal is dismissed.
* `opened`
_(Type: `promise`)_ -
Is resolved when a modal gets opened after downloading content's template and resolving all variables.
* `closed`
_(Type: `promise`)_ -
Is resolved when a modal is closed and the animation completes.
* `rendered`
_(Type: `promise`)_ -
Is resolved when a modal is rendered.
---
The scope associated with modal's content is augmented with:
* `$close(result)`
_(Type: `function`)_ -
A method that can be used to close a modal, passing a result.
* `$dismiss(reason)`
_(Type: `function`)_ -
A method that can be used to dismiss a modal, passing a reason.
Those methods make it easy to close a modal window without a need to create a dedicated controller.
Also, when using `bindToController`, you can define an `$onInit` method in the controller that will fire upon initialization.
---
Events fired:
* `$uibUnscheduledDestruction` -
This event is fired if the $scope is destroyed via unexpected mechanism, such as it being passed in the modal options and a $route/$state transition occurs. The modal will also be dismissed.
* `modal.closing` -
This event is broadcast to the modal scope before the modal closes. If the listener calls preventDefault() on the event, then the modal will remain open.
Also, the `$close` and `$dismiss` methods returns true if the event was executed. This event also includes a parameter for the result/reason and a boolean that indicates whether the modal is being closed (true) or dismissed.
##### UI Router resolves
If one wants to have the modal resolve using [UI Router's](https://github.com/angular-ui/ui-router) pre-1.0 resolve mechanism, one can call `$uibResolve.setResolver('$resolve')` in the configuration phase of the application. One can also provide a custom resolver as well, as long as the signature conforms to UI Router's [$resolve](http://angular-ui.github.io/ui-router/site/#/api/ui.router.util.$resolve).
When the modal is opened with a controller, a `$resolve` object is exposed on the template with the resolved values from the resolve object. If using the component option, see details on how to access this object in component section of the modal documentation.
================================================
FILE: src/modal/index-nocss.js
================================================
require('../multiMap');
require('../position/index-nocss.js');
require('../stackedMap');
require('../../template/modal/window.html.js');
require('./modal');
var MODULE_NAME = 'ui.bootstrap.module.modal';
angular.module(MODULE_NAME, ['ui.bootstrap.modal', 'uib/template/modal/window.html']);
module.exports = MODULE_NAME;
================================================
FILE: src/modal/index.js
================================================
require('../position/position.css');
module.exports = require('./index-nocss.js');
================================================
FILE: src/modal/modal.js
================================================
angular.module('ui.bootstrap.modal', ['ui.bootstrap.multiMap', 'ui.bootstrap.stackedMap', 'ui.bootstrap.position'])
/**
* Pluggable resolve mechanism for the modal resolve resolution
* Supports UI Router's $resolve service
*/
.provider('$uibResolve', function() {
var resolve = this;
this.resolver = null;
this.setResolver = function(resolver) {
this.resolver = resolver;
};
this.$get = ['$injector', '$q', function($injector, $q) {
var resolver = resolve.resolver ? $injector.get(resolve.resolver) : null;
return {
resolve: function(invocables, locals, parent, self) {
if (resolver) {
return resolver.resolve(invocables, locals, parent, self);
}
var promises = [];
angular.forEach(invocables, function(value) {
if (angular.isFunction(value) || angular.isArray(value)) {
promises.push($q.resolve($injector.invoke(value)));
} else if (angular.isString(value)) {
promises.push($q.resolve($injector.get(value)));
} else {
promises.push($q.resolve(value));
}
});
return $q.all(promises).then(function(resolves) {
var resolveObj = {};
var resolveIter = 0;
angular.forEach(invocables, function(value, key) {
resolveObj[key] = resolves[resolveIter++];
});
return resolveObj;
});
}
};
}];
})
/**
* A helper directive for the $modal service. It creates a backdrop element.
*/
.directive('uibModalBackdrop', ['$animate', '$injector', '$uibModalStack',
function($animate, $injector, $modalStack) {
return {
restrict: 'A',
compile: function(tElement, tAttrs) {
tElement.addClass(tAttrs.backdropClass);
return linkFn;
}
};
function linkFn(scope, element, attrs) {
if (attrs.modalInClass) {
$animate.addClass(element, attrs.modalInClass);
scope.$on($modalStack.NOW_CLOSING_EVENT, function(e, setIsAsync) {
var done = setIsAsync();
if (scope.modalOptions.animation) {
$animate.removeClass(element, attrs.modalInClass).then(done);
} else {
done();
}
});
}
}
}])
.directive('uibModalWindow', ['$uibModalStack', '$q', '$animateCss', '$document',
function($modalStack, $q, $animateCss, $document) {
return {
scope: {
index: '@'
},
restrict: 'A',
transclude: true,
templateUrl: function(tElement, tAttrs) {
return tAttrs.templateUrl || 'uib/template/modal/window.html';
},
link: function(scope, element, attrs) {
element.addClass(attrs.windowTopClass || '');
scope.size = attrs.size;
scope.close = function(evt) {
var modal = $modalStack.getTop();
if (modal && modal.value.backdrop &&
modal.value.backdrop !== 'static' &&
evt.target === evt.currentTarget) {
evt.preventDefault();
evt.stopPropagation();
$modalStack.dismiss(modal.key, 'backdrop click');
}
};
// moved from template to fix issue #2280
element.on('click', scope.close);
// This property is only added to the scope for the purpose of detecting when this directive is rendered.
// We can detect that by using this property in the template associated with this directive and then use
// {@link Attribute#$observe} on it. For more details please see {@link TableColumnResize}.
scope.$isRendered = true;
// Deferred object that will be resolved when this modal is rendered.
var modalRenderDeferObj = $q.defer();
// Resolve render promise post-digest
scope.$$postDigest(function() {
modalRenderDeferObj.resolve();
});
modalRenderDeferObj.promise.then(function() {
var animationPromise = null;
if (attrs.modalInClass) {
animationPromise = $animateCss(element, {
addClass: attrs.modalInClass
}).start();
scope.$on($modalStack.NOW_CLOSING_EVENT, function(e, setIsAsync) {
var done = setIsAsync();
$animateCss(element, {
removeClass: attrs.modalInClass
}).start().then(done);
});
}
$q.when(animationPromise).then(function() {
// Notify {@link $modalStack} that modal is rendered.
var modal = $modalStack.getTop();
if (modal) {
$modalStack.modalRendered(modal.key);
}
/**
* If something within the freshly-opened modal already has focus (perhaps via a
* directive that causes focus) then there's no need to try to focus anything.
*/
if (!($document[0].activeElement && element[0].contains($document[0].activeElement))) {
var inputWithAutofocus = element[0].querySelector('[autofocus]');
/**
* Auto-focusing of a freshly-opened modal element causes any child elements
* with the autofocus attribute to lose focus. This is an issue on touch
* based devices which will show and then hide the onscreen keyboard.
* Attempts to refocus the autofocus element via JavaScript will not reopen
* the onscreen keyboard. Fixed by updated the focusing logic to only autofocus
* the modal element if the modal does not contain an autofocus element.
*/
if (inputWithAutofocus) {
inputWithAutofocus.focus();
} else {
element[0].focus();
}
}
});
});
}
};
}])
.directive('uibModalAnimationClass', function() {
return {
compile: function(tElement, tAttrs) {
if (tAttrs.modalAnimation) {
tElement.addClass(tAttrs.uibModalAnimationClass);
}
}
};
})
.directive('uibModalTransclude', ['$animate', function($animate) {
return {
link: function(scope, element, attrs, controller, transclude) {
transclude(scope.$parent, function(clone) {
element.empty();
$animate.enter(clone, element);
});
}
};
}])
.factory('$uibModalStack', ['$animate', '$animateCss', '$document',
'$compile', '$rootScope', '$q', '$$multiMap', '$$stackedMap', '$uibPosition',
function($animate, $animateCss, $document, $compile, $rootScope, $q, $$multiMap, $$stackedMap, $uibPosition) {
var OPENED_MODAL_CLASS = 'modal-open';
var backdropDomEl, backdropScope;
var openedWindows = $$stackedMap.createNew();
var openedClasses = $$multiMap.createNew();
var $modalStack = {
NOW_CLOSING_EVENT: 'modal.stack.now-closing'
};
var topModalIndex = 0;
var previousTopOpenedModal = null;
var ARIA_HIDDEN_ATTRIBUTE_NAME = 'data-bootstrap-modal-aria-hidden-count';
//Modal focus behavior
var tabbableSelector = 'a[href], area[href], input:not([disabled]):not([tabindex=\'-1\']), ' +
'button:not([disabled]):not([tabindex=\'-1\']),select:not([disabled]):not([tabindex=\'-1\']), textarea:not([disabled]):not([tabindex=\'-1\']), ' +
'iframe, object, embed, *[tabindex]:not([tabindex=\'-1\']), *[contenteditable=true]';
var scrollbarPadding;
var SNAKE_CASE_REGEXP = /[A-Z]/g;
// TODO: extract into common dependency with tooltip
function snake_case(name) {
var separator = '-';
return name.replace(SNAKE_CASE_REGEXP, function(letter, pos) {
return (pos ? separator : '') + letter.toLowerCase();
});
}
function isVisible(element) {
return !!(element.offsetWidth ||
element.offsetHeight ||
element.getClientRects().length);
}
function backdropIndex() {
var topBackdropIndex = -1;
var opened = openedWindows.keys();
for (var i = 0; i < opened.length; i++) {
if (openedWindows.get(opened[i]).value.backdrop) {
topBackdropIndex = i;
}
}
// If any backdrop exist, ensure that it's index is always
// right below the top modal
if (topBackdropIndex > -1 && topBackdropIndex < topModalIndex) {
topBackdropIndex = topModalIndex;
}
return topBackdropIndex;
}
$rootScope.$watch(backdropIndex, function(newBackdropIndex) {
if (backdropScope) {
backdropScope.index = newBackdropIndex;
}
});
function removeModalWindow(modalInstance, elementToReceiveFocus) {
var modalWindow = openedWindows.get(modalInstance).value;
var appendToElement = modalWindow.appendTo;
//clean up the stack
openedWindows.remove(modalInstance);
previousTopOpenedModal = openedWindows.top();
if (previousTopOpenedModal) {
topModalIndex = parseInt(previousTopOpenedModal.value.modalDomEl.attr('index'), 10);
}
removeAfterAnimate(modalWindow.modalDomEl, modalWindow.modalScope, function() {
var modalBodyClass = modalWindow.openedClass || OPENED_MODAL_CLASS;
openedClasses.remove(modalBodyClass, modalInstance);
var areAnyOpen = openedClasses.hasKey(modalBodyClass);
appendToElement.toggleClass(modalBodyClass, areAnyOpen);
if (!areAnyOpen && scrollbarPadding && scrollbarPadding.heightOverflow && scrollbarPadding.scrollbarWidth) {
if (scrollbarPadding.originalRight) {
appendToElement.css({paddingRight: scrollbarPadding.originalRight + 'px'});
} else {
appendToElement.css({paddingRight: ''});
}
scrollbarPadding = null;
}
toggleTopWindowClass(true);
}, modalWindow.closedDeferred);
checkRemoveBackdrop();
//move focus to specified element if available, or else to body
if (elementToReceiveFocus && elementToReceiveFocus.focus) {
elementToReceiveFocus.focus();
} else if (appendToElement.focus) {
appendToElement.focus();
}
}
// Add or remove "windowTopClass" from the top window in the stack
function toggleTopWindowClass(toggleSwitch) {
var modalWindow;
if (openedWindows.length() > 0) {
modalWindow = openedWindows.top().value;
modalWindow.modalDomEl.toggleClass(modalWindow.windowTopClass || '', toggleSwitch);
}
}
function checkRemoveBackdrop() {
//remove backdrop if no longer needed
if (backdropDomEl && backdropIndex() === -1) {
var backdropScopeRef = backdropScope;
removeAfterAnimate(backdropDomEl, backdropScope, function() {
backdropScopeRef = null;
});
backdropDomEl = undefined;
backdropScope = undefined;
}
}
function removeAfterAnimate(domEl, scope, done, closedDeferred) {
var asyncDeferred;
var asyncPromise = null;
var setIsAsync = function() {
if (!asyncDeferred) {
asyncDeferred = $q.defer();
asyncPromise = asyncDeferred.promise;
}
return function asyncDone() {
asyncDeferred.resolve();
};
};
scope.$broadcast($modalStack.NOW_CLOSING_EVENT, setIsAsync);
// Note that it's intentional that asyncPromise might be null.
// That's when setIsAsync has not been called during the
// NOW_CLOSING_EVENT broadcast.
return $q.when(asyncPromise).then(afterAnimating);
function afterAnimating() {
if (afterAnimating.done) {
return;
}
afterAnimating.done = true;
$animate.leave(domEl).then(function() {
if (done) {
done();
}
domEl.remove();
if (closedDeferred) {
closedDeferred.resolve();
}
});
scope.$destroy();
}
}
$document.on('keydown', keydownListener);
$rootScope.$on('$destroy', function() {
$document.off('keydown', keydownListener);
});
function keydownListener(evt) {
if (evt.isDefaultPrevented()) {
return evt;
}
var modal = openedWindows.top();
if (modal) {
switch (evt.which) {
case 27: {
if (modal.value.keyboard) {
evt.preventDefault();
$rootScope.$apply(function() {
$modalStack.dismiss(modal.key, 'escape key press');
});
}
break;
}
case 9: {
var list = $modalStack.loadFocusElementList(modal);
var focusChanged = false;
if (evt.shiftKey) {
if ($modalStack.isFocusInFirstItem(evt, list) || $modalStack.isModalFocused(evt, modal)) {
focusChanged = $modalStack.focusLastFocusableElement(list);
}
} else {
if ($modalStack.isFocusInLastItem(evt, list)) {
focusChanged = $modalStack.focusFirstFocusableElement(list);
}
}
if (focusChanged) {
evt.preventDefault();
evt.stopPropagation();
}
break;
}
}
}
}
$modalStack.open = function(modalInstance, modal) {
var modalOpener = $document[0].activeElement,
modalBodyClass = modal.openedClass || OPENED_MODAL_CLASS;
toggleTopWindowClass(false);
// Store the current top first, to determine what index we ought to use
// for the current top modal
previousTopOpenedModal = openedWindows.top();
openedWindows.add(modalInstance, {
deferred: modal.deferred,
renderDeferred: modal.renderDeferred,
closedDeferred: modal.closedDeferred,
modalScope: modal.scope,
backdrop: modal.backdrop,
keyboard: modal.keyboard,
openedClass: modal.openedClass,
windowTopClass: modal.windowTopClass,
animation: modal.animation,
appendTo: modal.appendTo
});
openedClasses.put(modalBodyClass, modalInstance);
var appendToElement = modal.appendTo,
currBackdropIndex = backdropIndex();
if (currBackdropIndex >= 0 && !backdropDomEl) {
backdropScope = $rootScope.$new(true);
backdropScope.modalOptions = modal;
backdropScope.index = currBackdropIndex;
backdropDomEl = angular.element('
');
backdropDomEl.attr({
'class': 'modal-backdrop',
'ng-style': '{\'z-index\': 1040 + (index && 1 || 0) + index*10}',
'uib-modal-animation-class': 'fade',
'modal-in-class': 'in'
});
if (modal.backdropClass) {
backdropDomEl.addClass(modal.backdropClass);
}
if (modal.animation) {
backdropDomEl.attr('modal-animation', 'true');
}
$compile(backdropDomEl)(backdropScope);
$animate.enter(backdropDomEl, appendToElement);
if ($uibPosition.isScrollable(appendToElement)) {
scrollbarPadding = $uibPosition.scrollbarPadding(appendToElement);
if (scrollbarPadding.heightOverflow && scrollbarPadding.scrollbarWidth) {
appendToElement.css({paddingRight: scrollbarPadding.right + 'px'});
}
}
}
var content;
if (modal.component) {
content = document.createElement(snake_case(modal.component.name));
content = angular.element(content);
content.attr({
resolve: '$resolve',
'modal-instance': '$uibModalInstance',
close: '$close($value)',
dismiss: '$dismiss($value)'
});
} else {
content = modal.content;
}
// Set the top modal index based on the index of the previous top modal
topModalIndex = previousTopOpenedModal ? parseInt(previousTopOpenedModal.value.modalDomEl.attr('index'), 10) + 1 : 0;
var angularDomEl = angular.element('
');
angularDomEl.attr({
'class': 'modal',
'template-url': modal.windowTemplateUrl,
'window-top-class': modal.windowTopClass,
'role': 'dialog',
'aria-labelledby': modal.ariaLabelledBy,
'aria-describedby': modal.ariaDescribedBy,
'size': modal.size,
'index': topModalIndex,
'animate': 'animate',
'ng-style': '{\'z-index\': 1050 + $$topModalIndex*10, display: \'block\'}',
'tabindex': -1,
'uib-modal-animation-class': 'fade',
'modal-in-class': 'in'
}).append(content);
if (modal.windowClass) {
angularDomEl.addClass(modal.windowClass);
}
if (modal.animation) {
angularDomEl.attr('modal-animation', 'true');
}
appendToElement.addClass(modalBodyClass);
if (modal.scope) {
// we need to explicitly add the modal index to the modal scope
// because it is needed by ngStyle to compute the zIndex property.
modal.scope.$$topModalIndex = topModalIndex;
}
$animate.enter($compile(angularDomEl)(modal.scope), appendToElement);
openedWindows.top().value.modalDomEl = angularDomEl;
openedWindows.top().value.modalOpener = modalOpener;
applyAriaHidden(angularDomEl);
function applyAriaHidden(el) {
if (!el || el[0].tagName === 'BODY') {
return;
}
getSiblings(el).forEach(function(sibling) {
var elemIsAlreadyHidden = sibling.getAttribute('aria-hidden') === 'true',
ariaHiddenCount = parseInt(sibling.getAttribute(ARIA_HIDDEN_ATTRIBUTE_NAME), 10);
if (!ariaHiddenCount) {
ariaHiddenCount = elemIsAlreadyHidden ? 1 : 0;
}
sibling.setAttribute(ARIA_HIDDEN_ATTRIBUTE_NAME, ariaHiddenCount + 1);
sibling.setAttribute('aria-hidden', 'true');
});
return applyAriaHidden(el.parent());
function getSiblings(el) {
var children = el.parent() ? el.parent().children() : [];
return Array.prototype.filter.call(children, function(child) {
return child !== el[0];
});
}
}
};
function broadcastClosing(modalWindow, resultOrReason, closing) {
return !modalWindow.value.modalScope.$broadcast('modal.closing', resultOrReason, closing).defaultPrevented;
}
function unhideBackgroundElements() {
Array.prototype.forEach.call(
document.querySelectorAll('[' + ARIA_HIDDEN_ATTRIBUTE_NAME + ']'),
function(hiddenEl) {
var ariaHiddenCount = parseInt(hiddenEl.getAttribute(ARIA_HIDDEN_ATTRIBUTE_NAME), 10),
newHiddenCount = ariaHiddenCount - 1;
hiddenEl.setAttribute(ARIA_HIDDEN_ATTRIBUTE_NAME, newHiddenCount);
if (!newHiddenCount) {
hiddenEl.removeAttribute(ARIA_HIDDEN_ATTRIBUTE_NAME);
hiddenEl.removeAttribute('aria-hidden');
}
}
);
}
$modalStack.close = function(modalInstance, result) {
var modalWindow = openedWindows.get(modalInstance);
unhideBackgroundElements();
if (modalWindow && broadcastClosing(modalWindow, result, true)) {
modalWindow.value.modalScope.$$uibDestructionScheduled = true;
modalWindow.value.deferred.resolve(result);
removeModalWindow(modalInstance, modalWindow.value.modalOpener);
return true;
}
return !modalWindow;
};
$modalStack.dismiss = function(modalInstance, reason) {
var modalWindow = openedWindows.get(modalInstance);
unhideBackgroundElements();
if (modalWindow && broadcastClosing(modalWindow, reason, false)) {
modalWindow.value.modalScope.$$uibDestructionScheduled = true;
modalWindow.value.deferred.reject(reason);
removeModalWindow(modalInstance, modalWindow.value.modalOpener);
return true;
}
return !modalWindow;
};
$modalStack.dismissAll = function(reason) {
var topModal = this.getTop();
while (topModal && this.dismiss(topModal.key, reason)) {
topModal = this.getTop();
}
};
$modalStack.getTop = function() {
return openedWindows.top();
};
$modalStack.modalRendered = function(modalInstance) {
var modalWindow = openedWindows.get(modalInstance);
if (modalWindow) {
modalWindow.value.renderDeferred.resolve();
}
};
$modalStack.focusFirstFocusableElement = function(list) {
if (list.length > 0) {
list[0].focus();
return true;
}
return false;
};
$modalStack.focusLastFocusableElement = function(list) {
if (list.length > 0) {
list[list.length - 1].focus();
return true;
}
return false;
};
$modalStack.isModalFocused = function(evt, modalWindow) {
if (evt && modalWindow) {
var modalDomEl = modalWindow.value.modalDomEl;
if (modalDomEl && modalDomEl.length) {
return (evt.target || evt.srcElement) === modalDomEl[0];
}
}
return false;
};
$modalStack.isFocusInFirstItem = function(evt, list) {
if (list.length > 0) {
return (evt.target || evt.srcElement) === list[0];
}
return false;
};
$modalStack.isFocusInLastItem = function(evt, list) {
if (list.length > 0) {
return (evt.target || evt.srcElement) === list[list.length - 1];
}
return false;
};
$modalStack.loadFocusElementList = function(modalWindow) {
if (modalWindow) {
var modalDomE1 = modalWindow.value.modalDomEl;
if (modalDomE1 && modalDomE1.length) {
var elements = modalDomE1[0].querySelectorAll(tabbableSelector);
return elements ?
Array.prototype.filter.call(elements, function(element) {
return isVisible(element);
}) : elements;
}
}
};
return $modalStack;
}])
.provider('$uibModal', function() {
var $modalProvider = {
options: {
animation: true,
backdrop: true, //can also be false or 'static'
keyboard: true
},
$get: ['$rootScope', '$q', '$document', '$templateRequest', '$controller', '$uibResolve', '$uibModalStack',
function ($rootScope, $q, $document, $templateRequest, $controller, $uibResolve, $modalStack) {
var $modal = {};
function getTemplatePromise(options) {
return options.template ? $q.when(options.template) :
$templateRequest(angular.isFunction(options.templateUrl) ?
options.templateUrl() : options.templateUrl);
}
var promiseChain = null;
$modal.getPromiseChain = function() {
return promiseChain;
};
$modal.open = function(modalOptions) {
var modalResultDeferred = $q.defer();
var modalOpenedDeferred = $q.defer();
var modalClosedDeferred = $q.defer();
var modalRenderDeferred = $q.defer();
//prepare an instance of a modal to be injected into controllers and returned to a caller
var modalInstance = {
result: modalResultDeferred.promise,
opened: modalOpenedDeferred.promise,
closed: modalClosedDeferred.promise,
rendered: modalRenderDeferred.promise,
close: function (result) {
return $modalStack.close(modalInstance, result);
},
dismiss: function (reason) {
return $modalStack.dismiss(modalInstance, reason);
}
};
//merge and clean up options
modalOptions = angular.extend({}, $modalProvider.options, modalOptions);
modalOptions.resolve = modalOptions.resolve || {};
modalOptions.appendTo = modalOptions.appendTo || $document.find('body').eq(0);
if (!modalOptions.appendTo.length) {
throw new Error('appendTo element not found. Make sure that the element passed is in DOM.');
}
//verify options
if (!modalOptions.component && !modalOptions.template && !modalOptions.templateUrl) {
throw new Error('One of component or template or templateUrl options is required.');
}
var templateAndResolvePromise;
if (modalOptions.component) {
templateAndResolvePromise = $q.when($uibResolve.resolve(modalOptions.resolve, {}, null, null));
} else {
templateAndResolvePromise =
$q.all([getTemplatePromise(modalOptions), $uibResolve.resolve(modalOptions.resolve, {}, null, null)]);
}
function resolveWithTemplate() {
return templateAndResolvePromise;
}
// Wait for the resolution of the existing promise chain.
// Then switch to our own combined promise dependency (regardless of how the previous modal fared).
// Then add to $modalStack and resolve opened.
// Finally clean up the chain variable if no subsequent modal has overwritten it.
var samePromise;
samePromise = promiseChain = $q.all([promiseChain])
.then(resolveWithTemplate, resolveWithTemplate)
.then(function resolveSuccess(tplAndVars) {
var providedScope = modalOptions.scope || $rootScope;
var modalScope = providedScope.$new();
modalScope.$close = modalInstance.close;
modalScope.$dismiss = modalInstance.dismiss;
modalScope.$on('$destroy', function() {
if (!modalScope.$$uibDestructionScheduled) {
modalScope.$dismiss('$uibUnscheduledDestruction');
}
});
var modal = {
scope: modalScope,
deferred: modalResultDeferred,
renderDeferred: modalRenderDeferred,
closedDeferred: modalClosedDeferred,
animation: modalOptions.animation,
backdrop: modalOptions.backdrop,
keyboard: modalOptions.keyboard,
backdropClass: modalOptions.backdropClass,
windowTopClass: modalOptions.windowTopClass,
windowClass: modalOptions.windowClass,
windowTemplateUrl: modalOptions.windowTemplateUrl,
ariaLabelledBy: modalOptions.ariaLabelledBy,
ariaDescribedBy: modalOptions.ariaDescribedBy,
size: modalOptions.size,
openedClass: modalOptions.openedClass,
appendTo: modalOptions.appendTo
};
var component = {};
var ctrlInstance, ctrlInstantiate, ctrlLocals = {};
if (modalOptions.component) {
constructLocals(component, false, true, false);
component.name = modalOptions.component;
modal.component = component;
} else if (modalOptions.controller) {
constructLocals(ctrlLocals, true, false, true);
// the third param will make the controller instantiate later,private api
// @see https://github.com/angular/angular.js/blob/master/src/ng/controller.js#L126
ctrlInstantiate = $controller(modalOptions.controller, ctrlLocals, true, modalOptions.controllerAs);
if (modalOptions.controllerAs && modalOptions.bindToController) {
ctrlInstance = ctrlInstantiate.instance;
ctrlInstance.$close = modalScope.$close;
ctrlInstance.$dismiss = modalScope.$dismiss;
angular.extend(ctrlInstance, {
$resolve: ctrlLocals.$scope.$resolve
}, providedScope);
}
ctrlInstance = ctrlInstantiate();
if (angular.isFunction(ctrlInstance.$onInit)) {
ctrlInstance.$onInit();
}
}
if (!modalOptions.component) {
modal.content = tplAndVars[0];
}
$modalStack.open(modalInstance, modal);
modalOpenedDeferred.resolve(true);
function constructLocals(obj, template, instanceOnScope, injectable) {
obj.$scope = modalScope;
obj.$scope.$resolve = {};
if (instanceOnScope) {
obj.$scope.$uibModalInstance = modalInstance;
} else {
obj.$uibModalInstance = modalInstance;
}
var resolves = template ? tplAndVars[1] : tplAndVars;
angular.forEach(resolves, function(value, key) {
if (injectable) {
obj[key] = value;
}
obj.$scope.$resolve[key] = value;
});
}
}, function resolveError(reason) {
modalOpenedDeferred.reject(reason);
modalResultDeferred.reject(reason);
})['finally'](function() {
if (promiseChain === samePromise) {
promiseChain = null;
}
});
return modalInstance;
};
return $modal;
}
]
};
return $modalProvider;
});
================================================
FILE: src/modal/test/modal.spec.js
================================================
describe('$uibResolve', function() {
beforeEach(module('ui.bootstrap.modal'));
it('should resolve invocables and return promise with object of resolutions', function() {
module(function($provide) {
$provide.factory('bar', function() {
return 'bar';
});
});
inject(function($q, $rootScope, $uibResolve) {
$uibResolve.resolve({
foo: 'bar',
bar: $q.resolve('baz'),
baz: function() {
return 'boo';
}
}).then(function(resolves) {
expect(resolves).toEqual({
foo: 'bar',
bar: 'baz',
baz: 'boo'
});
});
$rootScope.$digest();
});
});
describe('with custom resolver', function() {
beforeEach(module(function($provide, $uibResolveProvider) {
$provide.factory('$resolve', function() {
return {
resolve: jasmine.createSpy()
};
});
$uibResolveProvider.setResolver('$resolve');
}));
it('should call $resolve.resolve', inject(function($uibResolve, $resolve) {
$uibResolve.resolve({foo: 'bar'}, {}, null, null);
expect($resolve.resolve).toHaveBeenCalledWith({foo: 'bar'}, {}, null, null);
}));
});
});
describe('uibModalTransclude', function() {
var uibModalTranscludeDDO,
$animate;
beforeEach(module('ui.bootstrap.modal'));
beforeEach(module(function($provide) {
$animate = jasmine.createSpyObj('$animate', ['enter']);
$provide.value('$animate', $animate);
}));
beforeEach(inject(function(uibModalTranscludeDirective) {
uibModalTranscludeDDO = uibModalTranscludeDirective[0];
}));
describe('when initialised', function() {
var scope,
element,
transcludeSpy,
transcludeFn;
beforeEach(function() {
scope = {
$parent: 'parentScope'
};
element = jasmine.createSpyObj('containerElement', ['empty']);
transcludeSpy = jasmine.createSpy('transcludeSpy').and.callFake(function(scope, fn) {
transcludeFn = fn;
});
uibModalTranscludeDDO.link(scope, element, {}, {}, transcludeSpy);
});
it('should call the transclusion function', function() {
expect(transcludeSpy).toHaveBeenCalledWith(scope.$parent, jasmine.any(Function));
});
describe('transclusion callback', function() {
var transcludedContent;
beforeEach(function() {
transcludedContent = 'my transcluded content';
transcludeFn(transcludedContent);
});
it('should empty the element', function() {
expect(element.empty).toHaveBeenCalledWith();
});
it('should append the transcluded content', function() {
expect($animate.enter).toHaveBeenCalledWith(transcludedContent, element);
});
});
});
});
describe('$uibModal', function() {
var $animate, $controllerProvider, $rootScope, $document, $compile, $templateCache, $timeout, $q;
var $uibModal, $uibModalStack, $uibModalProvider;
beforeEach(module('ngAnimateMock'));
beforeEach(module('ui.bootstrap.modal'));
beforeEach(module('uib/template/modal/window.html'));
beforeEach(module(function(_$controllerProvider_, _$uibModalProvider_, $compileProvider) {
$controllerProvider = _$controllerProvider_;
$uibModalProvider = _$uibModalProvider_;
$compileProvider.directive('parentDirective', function() {
return {
controller: function() {
this.text = 'foo';
}
};
}).directive('childDirective', function() {
return {
require: '^parentDirective',
link: function(scope, elem, attrs, ctrl) {
scope.text = ctrl.text;
}
};
}).directive('focusMe', function() {
return {
link: function(scope, elem, attrs) {
elem.focus();
}
};
}).component('fooBar', {
bindings: {
resolve: '<',
modalInstance: '<',
close: '&',
dismiss: '&'
},
controller: angular.noop,
controllerAs: 'foobar',
template: '
Foo Bar
'
});
}));
beforeEach(inject(function(_$animate_, _$rootScope_, _$document_, _$compile_, _$templateCache_, _$timeout_, _$q_, _$uibModal_, _$uibModalStack_) {
$animate = _$animate_;
$rootScope = _$rootScope_;
$document = _$document_;
$compile = _$compile_;
$templateCache = _$templateCache_;
$timeout = _$timeout_;
$q = _$q_;
$uibModal = _$uibModal_;
$uibModalStack = _$uibModalStack_;
}));
beforeEach(function() {
jasmine.addMatchers({
toBeResolvedWith: function(util, customEqualityTesters) {
return {
compare: function(promise, expected) {
var called = false;
promise.then(function(result) {
expect(result).toEqual(expected);
if (result === expected) {
result.message = 'Expected "' + angular.mock.dump(result) + '" not to be resolved with "' + expected + '".';
} else {
result.message = 'Expected "' + angular.mock.dump(result) + '" to be resolved with "' + expected + '".';
}
}, function(result) {
fail('Expected "' + angular.mock.dump(result) + '" to be resolved with "' + expected + '".');
})['finally'](function() {
called = true;
});
$rootScope.$digest();
if (!called) {
fail('Expected "' + angular.mock.dump(result) + '" to be resolved with "' + expected + '".');
}
return {pass: true};
}
};
},
toBeRejectedWith: function(util, customEqualityTesters) {
return {
compare: function(promise, expected) {
var result = {};
var called = false;
promise.then(function(result) {
fail('Expected "' + angular.mock.dump(result) + '" to be rejected with "' + expected + '".');
}, function(result) {
expect(result).toEqual(expected);
if (result === expected) {
result.message = 'Expected "' + angular.mock.dump(result) + '" not to be rejected with "' + expected + '".';
} else {
result.message = 'Expected "' + angular.mock.dump(result) + '" to be rejected with "' + expected + '".';
}
})['finally'](function() {
called = true;
});
$rootScope.$digest();
if (!called) {
fail('Expected "' + angular.mock.dump(result) + '" to be rejected with "' + expected + '".');
}
return {pass: true};
}
};
},
toHaveModalOpenWithContent: function(util, customEqualityTesters) {
return {
compare: function(actual, content, selector) {
var contentToCompare, modalDomEls = actual.find('body > div.modal > div.modal-dialog > div.modal-content');
contentToCompare = selector ? modalDomEls.find(selector) : modalDomEls;
var result = {
pass: modalDomEls.css('display') === 'block' && contentToCompare.html() === content
};
if (result.pass) {
result.message = '"Expected "' + angular.mock.dump(modalDomEls) + '" not to be open with "' + content + '".';
} else {
result.message = '"Expected "' + angular.mock.dump(modalDomEls) + '" to be open with "' + content + '".';
}
return result;
}
};
},
toHaveModalsOpen: function(util, customEqualityTesters) {
return {
compare: function(actual, expected) {
var modalDomEls = actual.find('body > div.modal');
var result = {
pass: util.equals(modalDomEls.length, expected, customEqualityTesters)
};
if (result.pass) {
result.message = 'Expected "' + angular.mock.dump(modalDomEls) + '" not to have "' + expected + '" modals opened.';
} else {
result.message = 'Expected "' + angular.mock.dump(modalDomEls) + '" to have "' + expected + '" modals opened.';
}
return result;
}
};
},
toHaveBackdrop: function(util, customEqualityTesters) {
return {
compare: function(actual, expected) {
var backdropDomEls = actual.find('body > div.modal-backdrop');
var result = {
pass: util.equals(backdropDomEls.length, 1, customEqualityTesters)
};
if (result.pass) {
result.message = 'Expected "' + angular.mock.dump(backdropDomEls) + '" not to be a backdrop element".';
} else {
result.message = 'Expected "' + angular.mock.dump(backdropDomEls) + '" to be a backdrop element".';
}
return result;
}
};
}
});
});
afterEach(function () {
var body = $document.find('body');
body.find('div.modal').remove();
body.find('div.modal-backdrop').remove();
body.removeClass('modal-open');
$document.off('keydown');
});
function triggerKeyDown(element, keyCode, shiftKey) {
var e = $.Event('keydown');
e.srcElement = element[0];
e.which = keyCode;
e.shiftKey = shiftKey;
element.trigger(e);
}
function open(modalOptions, noFlush, noDigest) {
var modal = $uibModal.open(modalOptions);
modal.opened['catch'](angular.noop);
modal.result['catch'](angular.noop);
if (!noDigest) {
$rootScope.$digest();
if (!noFlush) {
$animate.flush();
}
}
return modal;
}
function close(modal, result, noFlush) {
var closed = modal.close(result);
$rootScope.$digest();
if (!noFlush) {
$animate.flush();
$rootScope.$digest();
$animate.flush();
$rootScope.$digest();
}
return closed;
}
function dismiss(modal, reason, noFlush) {
var closed = modal.dismiss(reason);
$rootScope.$digest();
if (!noFlush) {
$animate.flush();
$rootScope.$digest();
$animate.flush();
$rootScope.$digest();
}
return closed;
}
describe('basic scenarios with default options', function() {
it('should open and dismiss a modal with a minimal set of options', function() {
var modal = open({template: '
Content
'});
expect($document).toHaveModalsOpen(1);
expect($document).toHaveModalOpenWithContent('Content', 'div');
expect($document).toHaveBackdrop();
dismiss(modal, 'closing in test');
expect($document).toHaveModalsOpen(0);
expect($document).not.toHaveBackdrop();
});
it('should compile modal before inserting into DOM', function() {
var topModal;
var modalInstance = {
result: $q.defer(),
opened: $q.defer(),
closed: $q.defer(),
rendered: $q.defer(),
close: function (result) {
return $uibModalStack.close(modalInstance, result);
},
dismiss: function (reason) {
return $uibModalStack.dismiss(modalInstance, reason);
}
};
var expectedText = 'test';
$uibModalStack.open(modalInstance, {
appendTo: angular.element(document.body),
scope: $rootScope.$new(),
deferred: modalInstance.result,
renderDeferred: modalInstance.rendered,
closedDeferred: modalInstance.closed,
content: '
{{\'' + expectedText + '\'}}
'
});
topModal = $uibModalStack.getTop();
expect(topModal.value.modalDomEl.find('#test').length).toEqual(0);
expect(angular.element('#test').length).toEqual(0);
$rootScope.$digest();
expect(topModal.value.modalDomEl.find('#test').text()).toEqual(expectedText);
expect(angular.element('#test').text()).toEqual(expectedText);
$animate.flush();
close(modalInstance, 'closing in test', true);
});
it('should resolve rendered promise when animation is complete', function() {
var modalInstance = {
result: $q.defer(),
opened: $q.defer(),
closed: $q.defer(),
rendered: $q.defer(),
close: function (result) {
return $uibModalStack.close(modalInstance, result);
},
dismiss: function (reason) {
return $uibModalStack.dismiss(modalInstance, reason);
}
};
var rendered = false;
modalInstance.rendered.promise.then(function() {
rendered = true;
});
$uibModalStack.open(modalInstance, {
appendTo: angular.element(document.body),
scope: $rootScope.$new(),
deferred: modalInstance.result,
renderDeferred: modalInstance.rendered,
closedDeferred: modalInstance.closed,
content: '
test
'
});
$rootScope.$digest();
expect(rendered).toBe(false);
$animate.flush();
expect(rendered).toBe(true);
});
it('should not throw an exception on a second dismiss', function() {
var modal = open({template: '
Content
'});
expect($document).toHaveModalsOpen(1);
expect($document).toHaveModalOpenWithContent('Content', 'div');
expect($document).toHaveBackdrop();
dismiss(modal, 'closing in test');
expect($document).toHaveModalsOpen(0);
dismiss(modal, 'closing in test', true);
});
it('should not throw an exception on a second close', function() {
var modal = open({template: '
Content
'});
expect($document).toHaveModalsOpen(1);
expect($document).toHaveModalOpenWithContent('Content', 'div');
expect($document).toHaveBackdrop();
close(modal, 'closing in test');
expect($document).toHaveModalsOpen(0);
close(modal, 'closing in test', true);
});
it('should open a modal from templateUrl', function() {
$templateCache.put('content.html', '
URL Content
');
var modal = open({templateUrl: 'content.html'});
expect($document).toHaveModalsOpen(1);
expect($document).toHaveModalOpenWithContent('URL Content', 'div');
expect($document).toHaveBackdrop();
dismiss(modal, 'closing in test');
expect($document).toHaveModalsOpen(0);
expect($document).not.toHaveBackdrop();
});
it('should support closing on ESC', function() {
var modal = open({template: '
Content
'});
expect($document).toHaveModalsOpen(1);
triggerKeyDown($document, 27);
$animate.flush();
$rootScope.$digest();
$animate.flush();
$rootScope.$digest();
expect($document).toHaveModalsOpen(0);
});
it('should not close on ESC if event.preventDefault() was issued', function() {
var modal = open({template: '
' });
expect($document).toHaveModalsOpen(1);
var button = angular.element('button').on('keydown', preventKeyDown);
triggerKeyDown(button, 27);
$rootScope.$digest();
expect($document).toHaveModalsOpen(1);
button.off('keydown', preventKeyDown);
triggerKeyDown(button, 27);
$animate.flush();
$rootScope.$digest();
$animate.flush();
$rootScope.$digest();
expect($document).toHaveModalsOpen(0);
function preventKeyDown(evt) {
evt.preventDefault();
}
});
it('should support closing on backdrop click', function() {
var modal = open({template: '
Content
'});
expect($document).toHaveModalsOpen(1);
$document.find('body > div.modal').click();
$animate.flush();
$rootScope.$digest();
$animate.flush();
$rootScope.$digest();
expect($document).toHaveModalsOpen(0);
});
it('should return to the element which had focus before the dialog was invoked', function() {
var link = '
Link';
var element = angular.element(link);
angular.element(document.body).append(element);
element.focus();
expect(document.activeElement.tagName).toBe('A');
var modal = open({template: '
Content
'});
$rootScope.$digest();
expect(document.activeElement.className.split(' ')).toContain('modal');
expect($document).toHaveModalsOpen(1);
triggerKeyDown($document, 27);
$animate.flush();
$rootScope.$digest();
$animate.flush();
$rootScope.$digest();
expect(document.activeElement.tagName).toBe('A');
expect($document).toHaveModalsOpen(0);
element.remove();
});
it('should return to document.body if element which had focus before the dialog was invoked is gone, or is missing focus function', function() {
var link = '
Link';
var element = angular.element(link);
angular.element(document.body).append(element);
element.focus();
expect(document.activeElement.tagName).toBe('A');
var modal = open({template: '
Content
'});
$rootScope.$digest();
expect(document.activeElement.tagName).toBe('DIV');
expect($document).toHaveModalsOpen(1);
// Fake undefined focus function, happening in IE in certain
// iframe conditions. See issue 3639
element[0].focus = undefined;
triggerKeyDown($document, 27);
$animate.flush();
$rootScope.$digest();
$animate.flush();
$rootScope.$digest();
expect(document.activeElement.tagName).toBe('BODY');
expect($document).toHaveModalsOpen(0);
element.remove();
});
it('should resolve returned promise on close', function() {
var modal = open({template: '
Content
'});
close(modal, 'closed ok');
expect(modal.result).toBeResolvedWith('closed ok');
});
it('should reject returned promise on dismiss', function() {
var modal = open({template: '
Content
'});
dismiss(modal, 'esc');
expect(modal.result).toBeRejectedWith('esc');
});
it('should reject returned promise on unexpected closure', function() {
var scope = $rootScope.$new();
var modal = open({template: '
Content
', scope: scope});
scope.$destroy();
expect(modal.result).toBeRejectedWith('$uibUnscheduledDestruction');
$animate.flush();
$rootScope.$digest();
$animate.flush();
$rootScope.$digest();
expect($document).toHaveModalsOpen(0);
});
it('should resolve the closed promise when modal is closed', function() {
var modal = open({template: '
Content
'});
var closed = false;
close(modal, 'closed ok');
modal.closed.then(function() {
closed = true;
});
$rootScope.$digest();
expect(closed).toBe(true);
});
it('should resolve the closed promise when modal is dismissed', function() {
var modal = open({template: '
Content
'});
var closed = false;
dismiss(modal, 'esc');
modal.closed.then(function() {
closed = true;
});
$rootScope.$digest();
expect(closed).toBe(true);
});
it('should expose a promise linked to the templateUrl / resolve promises', function() {
var modal = open({template: '
Content
', resolve: {
ok: function() {return $q.when('ok');}
}}
);
expect(modal.opened).toBeResolvedWith(true);
});
it('should expose a promise linked to the templateUrl / resolve promises and reject it if needed', function() {
var modal = open({template: '
Content
', resolve: {
ok: function() {return $q.reject('ko');}
}}, true);
expect(modal.opened).toBeRejectedWith('ko');
});
it('should focus on the element that has autofocus attribute when the modal is open/reopen and the animations have finished', function() {
function openAndCloseModalWithAutofocusElement() {
var modal = open({template: '
'});
$rootScope.$digest();
expect(angular.element('#auto-focus-element')).toHaveFocus();
close(modal, 'closed ok');
expect(modal.result).toBeResolvedWith('closed ok');
}
openAndCloseModalWithAutofocusElement();
openAndCloseModalWithAutofocusElement();
});
it('should not focus on the element that has autofocus attribute when the modal is opened and something in the modal already has focus and the animations have finished', function() {
function openAndCloseModalWithAutofocusElement() {
var modal = open({template: '
'});
$rootScope.$digest();
expect(angular.element('#auto-focus-element')).not.toHaveFocus();
expect(angular.element('#pre-focus-element')).toHaveFocus();
close(modal, 'closed ok');
expect(modal.result).toBeResolvedWith('closed ok');
}
openAndCloseModalWithAutofocusElement();
openAndCloseModalWithAutofocusElement();
});
it('should wait until the in animation is finished before attempting to focus the modal or autofocus element', function() {
function openAndCloseModalWithAutofocusElement() {
var modal = open({template: '
'}, true, true);
expect(angular.element('#auto-focus-element')).not.toHaveFocus();
$rootScope.$digest();
$animate.flush();
expect(angular.element('#auto-focus-element')).toHaveFocus();
close(modal, 'closed ok');
expect(modal.result).toBeResolvedWith('closed ok');
}
function openAndCloseModalWithOutAutofocusElement() {
var link = '
Link';
var element = angular.element(link);
angular.element(document.body).append(element);
element.focus();
expect(document.activeElement.tagName).toBe('A');
var modal = open({template: '
'}, true, true);
expect(document.activeElement.tagName).toBe('A');
$rootScope.$digest();
$animate.flush();
expect(document.activeElement.className.split(' ')).toContain('modal');
close(modal, 'closed ok');
expect(modal.result).toBeResolvedWith('closed ok');
element.remove();
}
openAndCloseModalWithAutofocusElement();
openAndCloseModalWithOutAutofocusElement();
});
it('should change focus to first element when tab key was pressed', function() {
var initialPage = angular.element('
Outland link');
angular.element(document.body).append(initialPage);
initialPage.focus();
open({
template:'
' +
''
});
expect($document).toHaveModalsOpen(1);
var lastElement = angular.element(document.getElementById('tab-focus-button'));
lastElement.focus();
triggerKeyDown(lastElement, 9);
expect(document.activeElement.getAttribute('id')).toBe('tab-focus-link');
initialPage.remove();
});
it('should change focus to last element when shift+tab key is pressed', function() {
var initialPage = angular.element('Outland link');
angular.element(document.body).append(initialPage);
initialPage.focus();
open({
template:'
' +
''
});
$rootScope.$digest();
expect($document).toHaveModalsOpen(1);
triggerKeyDown(angular.element(document.activeElement), 9, true);
expect(document.activeElement.getAttribute('id')).toBe('tab-focus-button');
var lastElement = angular.element(document.getElementById('tab-focus-link'));
lastElement.focus();
triggerKeyDown(angular.element(document.activeElement), 9, true);
expect(document.activeElement.getAttribute('id')).toBe('tab-focus-button');
initialPage.remove();
});
it('should change focus to first element when tab key is pressed when keyboard is false', function() {
var initialPage = angular.element('Outland link');
angular.element(document.body).append(initialPage);
initialPage.focus();
open({
template:'
' +
'',
keyboard: false
});
expect($document).toHaveModalsOpen(1);
var lastElement = angular.element(document.getElementById('tab-focus-button'));
lastElement.focus();
triggerKeyDown(lastElement, 9);
expect(document.activeElement.getAttribute('id')).toBe('tab-focus-link');
initialPage.remove();
});
it('should change focus to last element when shift+tab keys are pressed when keyboard is false', function() {
var initialPage = angular.element('Outland link');
angular.element(document.body).append(initialPage);
initialPage.focus();
open({
template:'
' +
'',
keyboard: false
});
$rootScope.$digest();
expect($document).toHaveModalsOpen(1);
triggerKeyDown(angular.element(document.activeElement), 9, true);
expect(document.activeElement.getAttribute('id')).toBe('tab-focus-button');
var lastElement = angular.element(document.getElementById('tab-focus-link'));
lastElement.focus();
triggerKeyDown(angular.element(document.activeElement), 9, true);
expect(document.activeElement.getAttribute('id')).toBe('tab-focus-button');
initialPage.remove();
});
it('should change focus to next proper element when DOM changes and tab is pressed', function() {
var initialPage = angular.element('Outland link');
angular.element(document.body).append(initialPage);
initialPage.focus();
open({
template:'
abc' +
'
',
keyboard: false
});
$rootScope.$digest();
expect($document).toHaveModalsOpen(1);
$('#tab-focus-link3').focus();
expect(document.activeElement.getAttribute('id')).toBe('tab-focus-link3');
$('#tab-focus-button').remove();
triggerKeyDown(angular.element(document.activeElement), 9, false);
expect(document.activeElement.getAttribute('id')).toBe('tab-focus-link1');
initialPage.remove();
});
it('should change focus to next proper element when DOM changes and shift+tab is pressed', function() {
var initialPage = angular.element('
Outland link');
angular.element(document.body).append(initialPage);
initialPage.focus();
open({
template:'
abc' +
'
',
keyboard: false
});
$rootScope.$digest();
expect($document).toHaveModalsOpen(1);
$('#tab-focus-link1').focus();
expect(document.activeElement.getAttribute('id')).toBe('tab-focus-link1');
$('#tab-focus-button').remove();
triggerKeyDown(angular.element(document.activeElement), 9, true);
expect(document.activeElement.getAttribute('id')).toBe('tab-focus-link3');
initialPage.remove();
});
it('should change focus to next non-hidden element when tab is pressed', function() {
var initialPage = angular.element('
Outland link');
angular.element(document.body).append(initialPage);
initialPage.focus();
open({
template:'
abc' +
'
',
keyboard: false
});
$rootScope.$digest();
expect($document).toHaveModalsOpen(1);
$('#tab-focus-link3').focus();
expect(document.activeElement.getAttribute('id')).toBe('tab-focus-link3');
$('#tab-focus-button').css('display', 'none');
triggerKeyDown(angular.element(document.activeElement), 9, false);
expect(document.activeElement.getAttribute('id')).toBe('tab-focus-link1');
initialPage.remove();
});
it('should change focus to previous non-hidden element when shift+tab is pressed', function() {
var initialPage = angular.element('
Outland link');
angular.element(document.body).append(initialPage);
initialPage.focus();
open({
template:'
abc' +
'
',
keyboard: false
});
$rootScope.$digest();
expect($document).toHaveModalsOpen(1);
$('#tab-focus-link1').focus();
expect(document.activeElement.getAttribute('id')).toBe('tab-focus-link1');
$('#tab-focus-button').css('display', 'none');
triggerKeyDown(angular.element(document.activeElement), 9, true);
expect(document.activeElement.getAttribute('id')).toBe('tab-focus-link3');
initialPage.remove();
});
it('should change focus to next tabbable element when tab is pressed', function() {
var initialPage = angular.element('
Outland link');
angular.element(document.body).append(initialPage);
initialPage.focus();
open({
template:'
a' +
'
bc' +
'
',
keyboard: false
});
$rootScope.$digest();
expect($document).toHaveModalsOpen(1);
$('#tab-focus-link3').focus();
expect(document.activeElement.getAttribute('id')).toBe('tab-focus-link3');
triggerKeyDown(angular.element(document.activeElement), 9, false);
expect(document.activeElement.getAttribute('id')).toBe('tab-focus-link1');
initialPage.remove();
});
it('should change focus to previous tabbable element when shift+tab is pressed', function() {
var initialPage = angular.element('
Outland link');
angular.element(document.body).append(initialPage);
initialPage.focus();
open({
template:'
a' +
'
bc' +
'
',
keyboard: false
});
$rootScope.$digest();
expect($document).toHaveModalsOpen(1);
$('#tab-focus-link1').focus();
expect(document.activeElement.getAttribute('id')).toBe('tab-focus-link1');
triggerKeyDown(angular.element(document.activeElement), 9, true);
expect(document.activeElement.getAttribute('id')).toBe('tab-focus-link3');
initialPage.remove();
});
});
describe('default options can be changed in a provider', function() {
it('should allow overriding default options in a provider', function() {
$uibModalProvider.options.backdrop = false;
var modal = open({template: '
Content
'});
expect($document).toHaveModalOpenWithContent('Content', 'div');
expect($document).not.toHaveBackdrop();
});
it('should accept new objects with default options in a provider', function() {
$uibModalProvider.options = {
backdrop: false
};
var modal = open({template: '
Content
'});
expect($document).toHaveModalOpenWithContent('Content', 'div');
expect($document).not.toHaveBackdrop();
});
});
describe('option by option', function() {
describe('component', function() {
function getModalComponent($document) {
return $document.find('body > div.modal > div.modal-dialog > div.modal-content foo-bar');
}
it('should use as modal content', function() {
open({
component: 'fooBar'
});
var component = getModalComponent($document);
expect(component.html()).toBe('
Foo Bar
');
});
it('should bind expected values', function() {
var modal = open({
component: 'fooBar',
resolve: {
foo: function() {
return 'bar';
}
}
});
var component = getModalComponent($document);
var componentScope = component.isolateScope();
expect(componentScope.foobar.resolve.foo).toBe('bar');
expect(componentScope.foobar.modalInstance).toBe(modal);
expect(componentScope.foobar.close).toEqual(jasmine.any(Function));
expect(componentScope.foobar.dismiss).toEqual(jasmine.any(Function));
});
it('should close the modal', function() {
var modal = open({
component: 'fooBar',
resolve: {
foo: function() {
return 'bar';
}
}
});
var component = getModalComponent($document);
var componentScope = component.isolateScope();
componentScope.foobar.close({
$value: 'baz'
});
expect(modal.result).toBeResolvedWith('baz');
});
it('should dismiss the modal', function() {
var modal = open({
component: 'fooBar',
resolve: {
foo: function() {
return 'bar';
}
}
});
var component = getModalComponent($document);
var componentScope = component.isolateScope();
componentScope.foobar.dismiss({
$value: 'baz'
});
expect(modal.result).toBeRejectedWith('baz');
});
});
describe('template and templateUrl', function() {
it('should throw an error if none of component, template and templateUrl are provided', function() {
expect(function(){
var modal = open({});
}).toThrow(new Error('One of component or template or templateUrl options is required.'));
});
it('should not fail if a templateUrl contains leading / trailing white spaces', function() {
$templateCache.put('whitespace.html', '
Whitespaces
');
open({templateUrl: 'whitespace.html'});
expect($document).toHaveModalOpenWithContent('Whitespaces', 'div');
});
it('should accept template as a function', function() {
open({template: function() {
return '
From a function
';
}});
expect($document).toHaveModalOpenWithContent('From a function', 'div');
});
it('should not fail if a templateUrl as a function', function() {
$templateCache.put('whitespace.html', '
Whitespaces
');
open({templateUrl: function() {
return 'whitespace.html';
}});
expect($document).toHaveModalOpenWithContent('Whitespaces', 'div');
});
});
describe('controller', function() {
it('should accept controllers and inject modal instances', function() {
var TestCtrl = function($scope, $uibModalInstance) {
$scope.fromCtrl = 'Content from ctrl';
$scope.isModalInstance = angular.isObject($uibModalInstance) && angular.isFunction($uibModalInstance.close);
};
open({template: '
{{fromCtrl}} {{isModalInstance}}
', controller: TestCtrl});
expect($document).toHaveModalOpenWithContent('Content from ctrl true', 'div');
});
it('should accept controllerAs alias', function() {
$controllerProvider.register('TestCtrl', function($uibModalInstance) {
this.fromCtrl = 'Content from ctrl';
this.isModalInstance = angular.isObject($uibModalInstance) && angular.isFunction($uibModalInstance.close);
});
open({template: '
{{test.fromCtrl}} {{test.isModalInstance}}
', controller: 'TestCtrl as test'});
expect($document).toHaveModalOpenWithContent('Content from ctrl true', 'div');
});
it('should respect the controllerAs property as an alternative for the controller-as syntax', function() {
$controllerProvider.register('TestCtrl', function($uibModalInstance) {
this.fromCtrl = 'Content from ctrl';
this.isModalInstance = angular.isObject($uibModalInstance) && angular.isFunction($uibModalInstance.close);
});
open({template: '
{{test.fromCtrl}} {{test.isModalInstance}}
', controller: 'TestCtrl', controllerAs: 'test'});
expect($document).toHaveModalOpenWithContent('Content from ctrl true', 'div');
});
it('should allow defining in-place controller-as controllers', function() {
open({template: '
{{test.fromCtrl}} {{test.isModalInstance}}
', controller: function($uibModalInstance) {
this.fromCtrl = 'Content from ctrl';
this.isModalInstance = angular.isObject($uibModalInstance) && angular.isFunction($uibModalInstance.close);
}, controllerAs: 'test'});
expect($document).toHaveModalOpenWithContent('Content from ctrl true', 'div');
});
it('should allow usage of bindToController', function() {
var $scope = $rootScope.$new(true);
$scope.foo = 'bar';
open({
template: '
{{test.fromCtrl}} {{test.closeDismissPresent()}} {{test.foo}}
',
controller: function($uibModalInstance) {
expect(this.foo).toEqual($scope.foo);
this.fromCtrl = 'Content from ctrl';
this.closeDismissPresent = function() {
return angular.isFunction(this.$close) && angular.isFunction(this.$dismiss);
};
},
controllerAs: 'test',
bindToController: true,
scope: $scope
});
expect($document).toHaveModalOpenWithContent('Content from ctrl true bar', 'div');
});
it('should have $onInit called', function() {
var $scope = $rootScope.$new(true);
var $onInit = jasmine.createSpy('$onInit');
$scope.foo = 'bar';
open({
template: '
{{test.fromCtrl}} {{test.closeDismissPresent()}} {{test.foo}}
',
controller: function($uibModalInstance) {
this.$onInit = $onInit;
this.fromCtrl = 'Content from ctrl';
this.closeDismissPresent = function() {
return angular.isFunction(this.$close) && angular.isFunction(this.$dismiss);
};
},
controllerAs: 'test',
bindToController: true,
scope: $scope
});
expect($document).toHaveModalOpenWithContent('Content from ctrl true bar', 'div');
expect($onInit).toHaveBeenCalled();
});
});
describe('resolve', function() {
var ExposeCtrl = function($scope, value) {
$scope.value = value;
};
function modalDefinition(template, resolve) {
return {
template: template,
controller: ExposeCtrl,
resolve: resolve
};
}
it('should resolve simple values', function() {
open(modalDefinition('
{{value}}
', {
value: function() {
return 'Content from resolve';
}
}));
expect($document).toHaveModalOpenWithContent('Content from resolve', 'div');
});
it('should resolve string references to injectables', function() {
open({
controller: function($scope, $foo) {
$scope.value = 'Content from resolve';
expect($foo).toBe($uibModal);
},
resolve: {
$foo: '$uibModal'
},
template: '
{{value}}
'
});
expect($document).toHaveModalOpenWithContent('Content from resolve', 'div');
});
it('should resolve promises as promises', function() {
open({
controller: function($scope, $foo) {
$scope.value = 'Content from resolve';
expect($foo).toBe('bar');
},
resolve: {
$foo: $q.when('bar')
},
template: '
{{value}}
'
});
});
it('should delay showing modal if one of the resolves is a promise', function() {
open(modalDefinition('
{{value}}
', {
value: function() {
return $timeout(function() { return 'Promise'; }, 100);
}
}), true);
expect($document).toHaveModalsOpen(0);
$timeout.flush();
expect($document).toHaveModalOpenWithContent('Promise', 'div');
});
it('should not open dialog (and reject returned promise) if one of resolve fails', function() {
var deferred = $q.defer();
var modal = open(modalDefinition('
{{value}}
', {
value: function() {
return deferred.promise;
}
}), true);
expect($document).toHaveModalsOpen(0);
deferred.reject('error in test');
$rootScope.$digest();
expect($document).toHaveModalsOpen(0);
expect(modal.result).toBeRejectedWith('error in test');
});
it('should support injection with minification-safe syntax in resolve functions', function() {
open(modalDefinition('
{{value.id}}
', {
value: ['$locale', function(e) {
return e;
}]
}));
expect($document).toHaveModalOpenWithContent('en-us', 'div');
});
});
describe('scope', function() {
it('should use custom scope if provided', function() {
var $scope = $rootScope.$new();
$scope.fromScope = 'Content from custom scope';
open({
template: '
{{fromScope}}
',
scope: $scope
});
expect($document).toHaveModalOpenWithContent('Content from custom scope', 'div');
});
it('should create and use child of $rootScope if custom scope not provided', function() {
var scopeTailBefore = $rootScope.$$childTail;
$rootScope.fromScope = 'Content from root scope';
open({
template: '
{{fromScope}}
'
});
expect($document).toHaveModalOpenWithContent('Content from root scope', 'div');
});
it('should expose $resolve in template', function() {
open({
controller: function($scope) {},
resolve: {
$foo: function() {
return 'Content from resolve';
}
},
template: '
{{$resolve.$foo}}
'
});
expect($document).toHaveModalOpenWithContent('Content from resolve', 'div');
});
});
describe('keyboard', function () {
it('should not close modals if keyboard option is set to false', function() {
open({
template: '
No keyboard
',
keyboard: false
});
expect($document).toHaveModalsOpen(1);
triggerKeyDown($document, 27);
$rootScope.$digest();
expect($document).toHaveModalsOpen(1);
});
});
describe('backdrop', function() {
it('should not have any backdrop element if backdrop set to false', function() {
var modal = open({
template: '
No backdrop
',
backdrop: false
});
expect($document).toHaveModalOpenWithContent('No backdrop', 'div');
expect($document).not.toHaveBackdrop();
dismiss(modal);
expect($document).toHaveModalsOpen(0);
});
it('should not close modal on backdrop click if backdrop is specified as "static"', function() {
open({
template: '
Static backdrop
',
backdrop: 'static'
});
$document.find('body > div.modal-backdrop').click();
$rootScope.$digest();
expect($document).toHaveModalOpenWithContent('Static backdrop', 'div');
expect($document).toHaveBackdrop();
});
it('should contain backdrop in classes on each modal opening', function() {
var modal = open({ template: '
With backdrop
' });
var backdropEl = $document.find('body > div.modal-backdrop');
expect(backdropEl).toHaveClass('in');
dismiss(modal);
modal = open({ template: '
With backdrop
' });
backdropEl = $document.find('body > div.modal-backdrop');
expect(backdropEl).toHaveClass('in');
});
describe('custom backdrop classes', function () {
it('should support additional backdrop class as string', function() {
open({
template: '
With custom backdrop class
',
backdropClass: 'additional'
});
expect($document.find('div.modal-backdrop')).toHaveClass('additional');
});
});
});
describe('custom window classes', function() {
it('should support additional window class as string', function() {
open({
template: '
With custom window class
',
windowClass: 'additional'
});
expect($document.find('div.modal')).toHaveClass('additional');
});
});
describe('top window class', function () {
it('should support top class option', function () {
open({
template: '
With custom window top class
',
windowTopClass: 'top-class'
});
expect($document.find('div.modal')).toHaveClass('top-class');
});
});
describe('size', function() {
it('should support creating small modal dialogs', function() {
open({
template: '
Small modal dialog
',
size: 'sm'
});
expect($document.find('div.modal-dialog')).toHaveClass('modal-sm');
});
it('should support creating large modal dialogs', function() {
open({
template: '
Large modal dialog
',
size: 'lg'
});
expect($document.find('div.modal-dialog')).toHaveClass('modal-lg');
});
it('should support custom size modal dialogs', function() {
open({
template: '
Large modal dialog
',
size: 'custom'
});
expect($document.find('div.modal-dialog')).toHaveClass('modal-custom');
});
});
describe('animation', function() {
it('should have animation fade classes by default', function() {
open({
template: '
Small modal dialog
'
});
expect($document.find('.modal')).toHaveClass('fade');
expect($document.find('.modal-backdrop')).toHaveClass('fade');
});
it('should not have fade classes if animation false', function() {
open({
template: '
Small modal dialog
',
animation: false
});
expect($document.find('.modal')).not.toHaveClass('fade');
expect($document.find('.modal-backdrop')).not.toHaveClass('fade');
});
});
describe('appendTo', function() {
it('should be added to body by default', function() {
var modal = open({template: '
Content
'});
expect($document).toHaveModalsOpen(1);
expect($document).toHaveModalOpenWithContent('Content', 'div');
});
it('should not be added to body if appendTo is passed', function() {
var element = angular.element('
');
angular.element(document.body).append(element);
var modal = open({template: '
Content
', appendTo: element});
expect($document).not.toHaveModalOpenWithContent('Content', 'div');
element.remove();
});
it('should be added to appendTo element if appendTo is passed', function() {
var element = angular.element('
');
angular.element(document.body).append(element);
expect($document.find('section').children('div.modal').length).toBe(0);
open({template: '
Content
', appendTo: element});
expect($document.find('section').children('div.modal').length).toBe(1);
element.remove();
});
it('should throw error if appendTo element is not found', function() {
expect(function(){
open({template: '
Content
', appendTo: $document.find('aside')});
}).toThrow(new Error('appendTo element not found. Make sure that the element passed is in DOM.'));
});
it('should be removed from appendTo element when dismissed', function() {
var modal = open({template: '
Content
'});
expect($document).toHaveModalsOpen(1);
dismiss(modal);
expect($document).toHaveModalsOpen(0);
});
it('should allow requiring parent directive from appendTo target', function() {
var element = $compile('
')($rootScope);
angular.element(document.body).append(element);
open({template: '
{{text}}
', appendTo: element});
expect($document.find('[child-directive]').text()).toBe('foo');
element.remove();
});
});
describe('openedClass', function() {
var body;
beforeEach(function() {
body = $document.find('body');
});
it('should add the modal-open class to the body element by default', function() {
open({
template: '
dummy modal
'
});
expect(body).toHaveClass('modal-open');
});
it('should add the custom class to the body element', function() {
open({
template: '
dummy modal
',
openedClass: 'foo'
});
expect(body).toHaveClass('foo');
expect(body).not.toHaveClass('modal-open');
});
it('should remove the custom class on closing of modal after animations have completed', function() {
var modal = open({
template: '
dummy modal
',
openedClass: 'foo'
});
expect(body).toHaveClass('foo');
close(modal, null, true);
expect(body).toHaveClass('foo');
$animate.flush();
$rootScope.$digest();
$animate.flush();
$rootScope.$digest();
expect(body).not.toHaveClass('foo');
});
it('should add multiple custom classes to the body element and remove appropriately', function() {
var modal1 = open({
template: '
dummy modal
',
openedClass: 'foo'
});
expect(body).toHaveClass('foo');
expect(body).not.toHaveClass('modal-open');
var modal2 = open({
template: '
dummy modal
',
openedClass: 'bar'
});
expect(body).toHaveClass('foo');
expect(body).toHaveClass('bar');
expect(body).not.toHaveClass('modal-open');
var modal3 = open({
template: '
dummy modal
',
openedClass: 'foo'
});
expect(body).toHaveClass('foo');
expect(body).toHaveClass('bar');
expect(body).not.toHaveClass('modal-open');
close(modal1);
expect(body).toHaveClass('foo');
expect(body).toHaveClass('bar');
expect(body).not.toHaveClass('modal-open');
close(modal2);
expect(body).toHaveClass('foo');
expect(body).not.toHaveClass('bar');
expect(body).not.toHaveClass('modal-open');
close(modal3);
expect(body).not.toHaveClass('foo');
expect(body).not.toHaveClass('bar');
expect(body).not.toHaveClass('modal-open');
});
it('should not add the modal-open class if modal is closed before animation', function() {
var modal = open({
template: '
dummy modal
'
}, true);
close(modal);
expect(body).not.toHaveClass('modal-open');
});
});
describe('ariaLabelledBy', function() {
it('should add the aria-labelledby property to the modal', function() {
open({
template: '
Modal Label
Modal description
',
ariaLabelledBy: 'modal-label'
});
expect($document.find('.modal').attr('aria-labelledby')).toEqual('modal-label');
});
});
describe('ariaDescribedBy', function() {
it('should add the aria-describedby property to the modal', function() {
open({
template: '
Modal Label
Modal description
',
ariaDescribedBy: 'modal-description'
});
expect($document.find('.modal').attr('aria-describedby')).toEqual('modal-description');
});
});
});
describe('modal window', function() {
it('should not use transclusion scope for modals content - issue 2110', function() {
$rootScope.animate = false;
$compile('
')($rootScope);
$rootScope.$digest();
expect($rootScope.foo).toBeTruthy();
});
it('should support window top class', function () {
$rootScope.animate = false;
var windowEl = $compile('
content
')($rootScope);
$rootScope.$digest();
expect(windowEl).toHaveClass('test');
expect(windowEl).toHaveClass('foo');
});
it('should support custom template url', inject(function($templateCache) {
$templateCache.put('window.html', '
');
var windowEl = $compile('
content
')($rootScope);
$rootScope.$digest();
expect(windowEl.html()).toBe('
content
');
}));
});
describe('multiple modals', function() {
it('should allow opening of multiple modals', function() {
var modal1 = open({template: '
Modal1
'});
var modal2 = open({template: '
Modal2
'});
expect($document).toHaveModalsOpen(2);
dismiss(modal2);
expect($document).toHaveModalsOpen(1);
expect($document).toHaveModalOpenWithContent('Modal1', 'div');
dismiss(modal1);
expect($document).toHaveModalsOpen(0);
});
it('should be able to dismiss all modals at once', function() {
var modal1 = open({template: '
Modal1
'});
var modal2 = open({template: '
Modal2
'});
expect($document).toHaveModalsOpen(2);
$uibModalStack.dismissAll();
$animate.flush();
$animate.flush();
expect($document).toHaveModalsOpen(0);
});
it('should not close any modals on ESC if the topmost one does not allow it', function() {
var modal1 = open({template: '
Modal1
'});
var modal2 = open({template: '
Modal2
', keyboard: false});
triggerKeyDown($document, 27);
$rootScope.$digest();
expect($document).toHaveModalsOpen(2);
});
it('should not close any modals on click if a topmost modal does not have backdrop', function() {
var modal1 = open({template: '
Modal1
'});
var modal2 = open({template: '
Modal2
', backdrop: false});
$document.find('body > div.modal-backdrop').click();
$rootScope.$digest();
expect($document).toHaveModalsOpen(2);
});
it('should not interfere with default options', function() {
var modal1 = open({template: '
Modal1
', backdrop: false});
var modal2 = open({template: '
Modal2
'});
$rootScope.$digest();
expect($document).toHaveBackdrop();
});
it('should add "modal-open" class when a modal gets opened', function() {
var body = $document.find('body');
expect(body).not.toHaveClass('modal-open');
var modal1 = open({template: '
Content1
'});
expect(body).toHaveClass('modal-open');
var modal2 = open({template: '
Content1
'});
expect(body).toHaveClass('modal-open');
dismiss(modal1);
expect(body).toHaveClass('modal-open');
dismiss(modal2);
expect(body).not.toHaveClass('modal-open');
});
it('should return to the element which had focus before the dialog is invoked', function() {
var link = '
Link';
var element = angular.element(link);
angular.element(document.body).append(element);
element.focus();
expect(document.activeElement.tagName).toBe('A');
var modal1 = open({template: '
Modal1
'});
$rootScope.$digest();
document.getElementById('focus').focus();
expect(document.activeElement.tagName).toBe('BUTTON');
expect($document).toHaveModalsOpen(1);
var modal2 = open({template: '
Modal2
'});
$rootScope.$digest();
expect(document.activeElement.tagName).toBe('DIV');
expect($document).toHaveModalsOpen(2);
dismiss(modal2);
expect(document.activeElement.tagName).toBe('BUTTON');
expect($document).toHaveModalsOpen(1);
dismiss(modal1);
expect(document.activeElement.tagName).toBe('A');
expect($document).toHaveModalsOpen(0);
element.remove();
});
it('should open modals and resolve the opened promises in order', function() {
// Opens a modal for each element in array order.
// Order is an array of non-repeating integers from 0..length-1 representing when to resolve that modal's promise.
// For example [1,2,0] would resolve the 3rd modal's promise first and the 2nd modal's promise last.
// Tests that the modals are added to $uibModalStack and that each resolves its "opened" promise sequentially.
// If an element is {reject:n} then n is still the order, but the corresponding promise is rejected.
// A rejection earlier in the open sequence should not affect modals opened later.
function test(order) {
var ds = []; // {index, deferred, reject}
var expected = ''; // 0..length-1
var actual = '';
angular.forEach(order, function(x, i) {
var reject = x.reject !== undefined;
if (reject) {
x = x.reject;
} else {
expected += i;
}
ds[x] = {index: i, deferred: $q.defer(), reject: reject};
var scope = $rootScope.$new();
var failed = false;
scope.index = i;
open({
template: '
' + i + '
',
scope: scope,
resolve: {
x: function() { return ds[x].deferred.promise['catch'](function () {
failed = true;
}); }
}
}, true).opened.then(function() {
expect($uibModalStack.getTop().value.modalScope.index).toEqual(i);
if (!failed) { actual += i; }
});
});
angular.forEach(ds, function(d, i) {
if (d.reject) {
d.deferred.reject('rejected:' + d.index);
} else {
d.deferred.resolve('resolved:' + d.index);
}
$rootScope.$digest();
});
expect(actual).toEqual(expected);
expect($uibModal.getPromiseChain()).toEqual(null);
}
// Calls emit n! times on arrays of length n containing all non-repeating permutations of the integers 0..n-1.
function permute(n, emit) {
if (n < 1 || typeof emit !== 'function') {
return;
}
var a = [];
function _permute(depth) {
index: for (var i = 0; i < n; i++) {
for (var j = 0; j < depth; j++) {
if (a[j] === i) {
continue index; // already used
}
}
a[depth] = i;
if (depth + 1 === n) {
emit(angular.copy(a));
} else {
_permute(depth + 1);
}
}
}
_permute(0);
}
permute(2, function(a) {
test(a);
});
permute(2, function(a) {
test(a.map(function(x, i) {
return {reject:x};
}));
});
permute(2, function(a) {
test(a.map(function(x, i) {
return i === 0 ? {reject: x} : x;
}));
});
permute(3, function(a) {
test(a);
});
permute(3, function(a) {
test(a.map(function(x, i) {
return {reject: x};
}));
});
permute(3, function(a) {
test(a.map(function(x, i) {
return i === 0 ? {reject: x} : x;
}));
});
permute(3, function(a) {
test(a.map(function(x, i) {
return i === 1 ? {reject: x} : x;
}));
});
$animate.flush();
});
it('should have top class only on top window', function () {
var modal1 = open({template: '
Content1
', windowClass: 'modal1', windowTopClass: 'modal-top'});
expect($document.find('div.modal1')).toHaveClass('modal-top');
expect($document).toHaveModalsOpen(1);
var modal2 = open({template: '
Content1
', windowClass: 'modal2', windowTopClass: 'modal-top'});
expect($document.find('div.modal1')).not.toHaveClass('modal-top');
expect($document.find('div.modal2')).toHaveClass('modal-top');
expect($document).toHaveModalsOpen(2);
var modal3 = open({template: '
Content1
', windowClass: 'modal3', windowTopClass: 'modal-top'});
expect($document.find('div.modal1')).not.toHaveClass('modal-top');
expect($document.find('div.modal2')).not.toHaveClass('modal-top');
expect($document.find('div.modal3')).toHaveClass('modal-top');
expect($document).toHaveModalsOpen(3);
dismiss(modal2);
expect($document.find('div.modal1')).not.toHaveClass('modal-top');
expect($document.find('div.modal3')).toHaveClass('modal-top');
expect($document).toHaveModalsOpen(2);
close(modal3);
expect($document.find('div.modal1')).toHaveClass('modal-top');
expect($document).toHaveModalsOpen(1);
});
it('should have top modal with highest index', function() {
var modal2Index = null;
var modal3Index = null;
var modal1Instance = {
result: $q.defer(),
opened: $q.defer(),
closed: $q.defer(),
rendered: $q.defer(),
close: function(result) {
return $uibModalStack.close(modal1Instance, result);
},
dismiss: function(reason) {
return $uibModalStack.dismiss(modal1Instance, reason);
}
};
var modal2Instance = {
result: $q.defer(),
opened: $q.defer(),
closed: $q.defer(),
rendered: $q.defer(),
close: function(result) {
return $uibModalStack.close(modal2Instance, result);
},
dismiss: function(reason) {
return $uibModalStack.dismiss(modal2Instance, reason);
}
};
var modal3Instance = {
result: $q.defer(),
opened: $q.defer(),
closed: $q.defer(),
rendered: $q.defer(),
close: function(result) {
return $uibModalStack.close(modal13nstance, result);
},
dismiss: function(reason) {
return $uibModalStack.dismiss(modal3Instance, reason);
}
};
var modal1 = $uibModalStack.open(modal1Instance, {
appendTo: angular.element(document.body),
scope: $rootScope.$new(),
deferred: modal1Instance.result,
renderDeferred: modal1Instance.rendered,
closedDeferred: modal1Instance.closed,
content: '
Modal1
'
});
$rootScope.$digest();
$animate.flush();
expect($document).toHaveModalsOpen(1);
expect(parseInt($uibModalStack.getTop().value.modalDomEl.attr('index'), 10)).toEqual(0);
var modal2 = $uibModalStack.open(modal2Instance, {
appendTo: angular.element(document.body),
scope: $rootScope.$new(),
deferred: modal2Instance.result,
renderDeferred: modal2Instance.rendered,
closedDeferred: modal2Instance.closed,
content: '
Modal2
'
});
modal2Instance.rendered.promise.then(function() {
modal2Index = parseInt($uibModalStack.getTop().value.modalDomEl.attr('index'), 10);
});
$rootScope.$digest();
$animate.flush();
expect($document).toHaveModalsOpen(2);
expect(modal2Index).toEqual(1);
close(modal1Instance);
expect($document).toHaveModalsOpen(1);
var modal3 = $uibModalStack.open(modal3Instance, {
appendTo: angular.element(document.body),
scope: $rootScope.$new(),
deferred: modal3Instance.result,
renderDeferred: modal3Instance.rendered,
closedDeferred: modal3Instance.closed,
content: '
Modal3
'
});
modal3Instance.rendered.promise.then(function() {
modal3Index = parseInt($uibModalStack.getTop().value.modalDomEl.attr('index'), 10);
});
$rootScope.$digest();
$animate.flush();
expect($document).toHaveModalsOpen(2);
expect(modal3Index).toEqual(2);
expect(modal2Index).toBeLessThan(modal3Index);
});
it('should have top modal with highest z-index', function() {
var modal2zIndex = null;
var modal3zIndex = null;
var modal1Instance = {
result: $q.defer(),
opened: $q.defer(),
closed: $q.defer(),
rendered: $q.defer(),
close: function(result) {
return $uibModalStack.close(modal1Instance, result);
},
dismiss: function(reason) {
return $uibModalStack.dismiss(modal1Instance, reason);
}
};
var modal2Instance = {
result: $q.defer(),
opened: $q.defer(),
closed: $q.defer(),
rendered: $q.defer(),
close: function(result) {
return $uibModalStack.close(modal2Instance, result);
},
dismiss: function(reason) {
return $uibModalStack.dismiss(modal2Instance, reason);
}
};
var modal3Instance = {
result: $q.defer(),
opened: $q.defer(),
closed: $q.defer(),
rendered: $q.defer(),
close: function(result) {
return $uibModalStack.close(modal3Instance, result);
},
dismiss: function(reason) {
return $uibModalStack.dismiss(modal3Instance, reason);
}
};
var modal1 = $uibModalStack.open(modal1Instance, {
appendTo: angular.element(document.body),
scope: $rootScope.$new(),
deferred: modal1Instance.result,
renderDeferred: modal1Instance.rendered,
closedDeferred: modal1Instance.closed,
content: '
Modal1
'
});
$rootScope.$digest();
$animate.flush();
expect($document).toHaveModalsOpen(1);
expect(+$uibModalStack.getTop().value.modalDomEl[0].style.zIndex).toBe(1050);
var modal2 = $uibModalStack.open(modal2Instance, {
appendTo: angular.element(document.body),
scope: $rootScope.$new(),
deferred: modal2Instance.result,
renderDeferred: modal2Instance.rendered,
closedDeferred: modal2Instance.closed,
content: '
Modal2
'
});
modal2Instance.rendered.promise.then(function() {
modal2zIndex = +$uibModalStack.getTop().value.modalDomEl[0].style.zIndex;
});
$rootScope.$digest();
$animate.flush();
expect($document).toHaveModalsOpen(2);
expect(modal2zIndex).toBe(1060);
close(modal1Instance);
expect($document).toHaveModalsOpen(1);
var modal3 = $uibModalStack.open(modal3Instance, {
appendTo: angular.element(document.body),
scope: $rootScope.$new(),
deferred: modal3Instance.result,
renderDeferred: modal3Instance.rendered,
closedDeferred: modal3Instance.closed,
content: '
Modal3
'
});
modal3Instance.rendered.promise.then(function() {
modal3zIndex = +$uibModalStack.getTop().value.modalDomEl[0].style.zIndex;
});
$rootScope.$digest();
$animate.flush();
expect($document).toHaveModalsOpen(2);
expect(modal3zIndex).toBe(1070);
expect(modal2zIndex).toBeLessThan(modal3zIndex);
});
});
describe('modal.closing event', function() {
it('should close the modal contingent on the modal.closing event and return whether the modal closed', function() {
var preventDefault;
var modal;
var template = '
content
';
function TestCtrl($scope) {
$scope.$on('modal.closing', function(event, resultOrReason, closing) {
if (preventDefault) {
event.preventDefault();
}
});
}
modal = open({template: template, controller: TestCtrl});
preventDefault = true;
expect(close(modal, 'result', true)).toBeFalsy();
expect($document).toHaveModalsOpen(1);
preventDefault = false;
expect(close(modal, 'result')).toBeTruthy();
expect($document).toHaveModalsOpen(0);
modal = open({template: template, controller: TestCtrl});
preventDefault = true;
expect(dismiss(modal, 'result', true)).toBeFalsy();
expect($document).toHaveModalsOpen(1);
preventDefault = false;
expect(dismiss(modal, 'result')).toBeTruthy();
expect($document).toHaveModalsOpen(0);
});
it('should trigger modal.closing and pass result/reason and closing parameters to the event', function() {
var called;
called = false;
close(open({
template: '
content
',
controller: function($scope) {
$scope.$on('modal.closing', function(event, resultOrReason, closing) {
called = true;
expect(resultOrReason).toBe('result');
expect(closing).toBeTruthy();
});
}
}), 'result');
expect(called).toBeTruthy();
called = false;
dismiss(open({
template: '
content
',
controller: function($scope) {
$scope.$on('modal.closing', function(event, resultOrReason, closing) {
called = true;
expect(resultOrReason).toBe('reason');
expect(closing).toBeFalsy();
});
}
}), 'reason');
expect(called).toBeTruthy();
});
});
});
================================================
FILE: src/multiMap/index.js
================================================
require('./multiMap.js');
================================================
FILE: src/multiMap/multiMap.js
================================================
angular.module('ui.bootstrap.multiMap', [])
/**
* A helper, internal data structure that stores all references attached to key
*/
.factory('$$multiMap', function() {
return {
createNew: function() {
var map = {};
return {
entries: function() {
return Object.keys(map).map(function(key) {
return {
key: key,
value: map[key]
};
});
},
get: function(key) {
return map[key];
},
hasKey: function(key) {
return !!map[key];
},
keys: function() {
return Object.keys(map);
},
put: function(key, value) {
if (!map[key]) {
map[key] = [];
}
map[key].push(value);
},
remove: function(key, value) {
var values = map[key];
if (!values) {
return;
}
var idx = values.indexOf(value);
if (idx !== -1) {
values.splice(idx, 1);
}
if (!values.length) {
delete map[key];
}
}
};
}
};
});
================================================
FILE: src/multiMap/test/multiMap.spec.js
================================================
describe('multi map', function() {
var multiMap;
beforeEach(module('ui.bootstrap.multiMap'));
beforeEach(inject(function($$multiMap) {
multiMap = $$multiMap.createNew();
}));
it('should add and remove objects by key', function() {
multiMap.put('foo', 'bar');
expect(multiMap.get('foo')).toEqual(['bar']);
multiMap.put('foo', 'baz');
expect(multiMap.get('foo')).toEqual(['bar', 'baz']);
multiMap.remove('foo', 'bar');
expect(multiMap.get('foo')).toEqual(['baz']);
multiMap.remove('foo', 'baz');
expect(multiMap.hasKey('foo')).toBe(false);
});
it('should support getting the keys', function() {
multiMap.put('foo', 'bar');
multiMap.put('baz', 'boo');
expect(multiMap.keys()).toEqual(['foo', 'baz']);
});
it('should return all entries', function() {
multiMap.put('foo', 'bar');
multiMap.put('foo', 'bar2');
multiMap.put('baz', 'boo');
expect(multiMap.entries()).toEqual([
{
key: 'foo',
value: ['bar', 'bar2']
},
{
key: 'baz',
value: ['boo']
}
]);
});
it('should preserve semantic of an empty key', function() {
expect(multiMap.get('key')).toBeUndefined();
});
it('should respect removal of non-existing elements', function() {
expect(multiMap.remove('foo', 'bar')).toBeUndefined();
});
});
================================================
FILE: src/pager/docs/demo.html
================================================
Pager
You are currently on page {{currentPage}}
================================================
FILE: src/pager/docs/demo.js
================================================
angular.module('ui.bootstrap.demo').controller('PagerDemoCtrl', function($scope) {
$scope.totalItems = 64;
$scope.currentPage = 4;
});
================================================
FILE: src/pager/docs/readme.md
================================================
A lightweight pager directive that is focused on providing previous/next paging functionality
### uib-pager settings
* `align`
C
_(Default: `true`)_ -
Whether to align each link to the sides.
* `items-per-page`
$
C
_(Default: `10`)_ -
Maximum number of items per page. A value less than one indicates all items on one page.
* `next-text`
C
_(Default: `Next »`)_ -
Text for Next button.
* `ng-disabled`
$
_(Default: `false`)_ -
Used to disable the pager component.
* `ng-model`
$
-
Current page number. First page is 1.
* `num-pages`
$
readonly
_(Default: `angular.noop`)_ -
An optional expression assigned the total number of pages to display.
* `previous-text`
C
_(Default: `« Previous`)_ -
Text for Previous button.
* `template-url`
_(Default: `uib/template/pager/pager.html`)_ -
Override the template for the component with a custom provided template.
* `total-items`
$
-
Total number of items in all pages.
================================================
FILE: src/pager/index.js
================================================
require('../paging');
require('../tabindex');
require('../../template/pager/pager.html.js');
require('./pager');
var MODULE_NAME = 'ui.bootstrap.module.pager';
angular.module(MODULE_NAME, ['ui.bootstrap.pager', 'uib/template/pager/pager.html']);
module.exports = MODULE_NAME;
================================================
FILE: src/pager/pager.js
================================================
angular.module('ui.bootstrap.pager', ['ui.bootstrap.paging', 'ui.bootstrap.tabindex'])
.controller('UibPagerController', ['$scope', '$attrs', 'uibPaging', 'uibPagerConfig', function($scope, $attrs, uibPaging, uibPagerConfig) {
$scope.align = angular.isDefined($attrs.align) ? $scope.$parent.$eval($attrs.align) : uibPagerConfig.align;
uibPaging.create(this, $scope, $attrs);
}])
.constant('uibPagerConfig', {
itemsPerPage: 10,
previousText: '« Previous',
nextText: 'Next »',
align: true
})
.directive('uibPager', ['uibPagerConfig', function(uibPagerConfig) {
return {
scope: {
totalItems: '=',
previousText: '@',
nextText: '@',
ngDisabled: '='
},
require: ['uibPager', '?ngModel'],
restrict: 'A',
controller: 'UibPagerController',
controllerAs: 'pager',
templateUrl: function(element, attrs) {
return attrs.templateUrl || 'uib/template/pager/pager.html';
},
link: function(scope, element, attrs, ctrls) {
element.addClass('pager');
var paginationCtrl = ctrls[0], ngModelCtrl = ctrls[1];
if (!ngModelCtrl) {
return; // do nothing if no ng-model
}
paginationCtrl.init(ngModelCtrl, uibPagerConfig);
}
};
}]);
================================================
FILE: src/pager/test/pager.spec.js
================================================
describe('pager directive', function() {
var $compile, $rootScope, $document, $templateCache, body, element;
beforeEach(module('ui.bootstrap.pager'));
beforeEach(module('uib/template/pager/pager.html'));
beforeEach(inject(function(_$compile_, _$rootScope_, _$document_, _$templateCache_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
$rootScope.total = 47; // 5 pages
$rootScope.currentPage = 3;
$document = _$document_;
$templateCache = _$templateCache_;
body = $document.find('body');
element = $compile('
')($rootScope);
$rootScope.$digest();
}));
function getPaginationBarSize() {
return element.find('li').length;
}
function getPaginationEl(index) {
return element.find('li').eq(index);
}
function clickPaginationEl(index) {
getPaginationEl(index).find('a').click();
}
function getPaginationLinkEl(elem, index) {
return elem.find('li').eq(index).find('a');
}
function updateCurrentPage(value) {
$rootScope.currentPage = value;
$rootScope.$digest();
}
it('has a "pager" css class', function() {
expect(element.hasClass('pager')).toBe(true);
});
it('contains 2 li elements', function() {
expect(getPaginationBarSize()).toBe(2);
expect(getPaginationEl(0).text()).toBe('« Previous');
expect(getPaginationEl(-1).text()).toBe('Next »');
});
it('aligns previous & next page', function() {
expect(getPaginationEl(0)).toHaveClass('previous');
expect(getPaginationEl(0)).not.toHaveClass('next');
expect(getPaginationEl(-1)).not.toHaveClass('previous');
expect(getPaginationEl(-1)).toHaveClass('next');
});
it('exposes the controller on the template', function() {
$templateCache.put('uib/template/pager/pager.html', '
{{pager.text}}
');
element = $compile('
')($rootScope);
$rootScope.$digest();
var ctrl = element.controller('uibPager');
expect(ctrl).toBeDefined();
ctrl.text = 'foo';
$rootScope.$digest();
expect(element.html()).toBe('
foo
');
});
it('disables the "previous" link if current page is 1', function() {
updateCurrentPage(1);
expect(getPaginationEl(0)).toHaveClass('disabled');
});
it('disables the "next" link if current page is num-pages', function() {
updateCurrentPage(5);
expect(getPaginationEl(-1)).toHaveClass('disabled');
});
it('changes currentPage if the "previous" link is clicked', function() {
clickPaginationEl(0);
expect($rootScope.currentPage).toBe(2);
});
it('changes currentPage if the "next" link is clicked', function() {
clickPaginationEl(-1);
expect($rootScope.currentPage).toBe(4);
});
it('does not change the current page on "previous" click if already at first page', function() {
updateCurrentPage(1);
clickPaginationEl(0);
expect($rootScope.currentPage).toBe(1);
});
it('does not change the current page on "next" click if already at last page', function() {
updateCurrentPage(5);
clickPaginationEl(-1);
expect($rootScope.currentPage).toBe(5);
});
it('executes the `ng-change` expression when an element is clicked', function() {
$rootScope.selectPageHandler = jasmine.createSpy('selectPageHandler');
element = $compile('
')($rootScope);
$rootScope.$digest();
clickPaginationEl(-1);
expect($rootScope.selectPageHandler).toHaveBeenCalled();
});
it('does not changes the number of pages when `total-items` changes', function() {
$rootScope.total = 73; // 8 pages
$rootScope.$digest();
expect(getPaginationBarSize()).toBe(2);
expect(getPaginationEl(0).text()).toBe('« Previous');
expect(getPaginationEl(-1).text()).toBe('Next »');
});
it('should blur the "next" link after it has been clicked', function() {
body.append(element);
var linkEl = getPaginationLinkEl(element, -1);
linkEl.focus();
expect(linkEl).toHaveFocus();
linkEl.click();
expect(linkEl).not.toHaveFocus();
element.remove();
});
it('should blur the "prev" link after it has been clicked', function() {
body.append(element);
var linkEl = getPaginationLinkEl(element, -1);
linkEl.focus();
expect(linkEl).toHaveFocus();
linkEl.click();
expect(linkEl).not.toHaveFocus();
element.remove();
});
it('allows custom templates', function() {
$templateCache.put('foo/bar.html', '
baz
');
element = $compile('
')($rootScope);
$rootScope.$digest();
expect(element.html()).toBe('
baz
');
});
describe('`items-per-page`', function() {
beforeEach(function() {
$rootScope.perpage = 5;
element = $compile('
')($rootScope);
$rootScope.$digest();
});
it('does not change the number of pages', function() {
expect(getPaginationBarSize()).toBe(2);
expect(getPaginationEl(0).text()).toBe('« Previous');
expect(getPaginationEl(-1).text()).toBe('Next »');
});
it('selects the last page when it is too big', function() {
$rootScope.perpage = 30;
$rootScope.$digest();
expect($rootScope.currentPage).toBe(2);
expect(getPaginationBarSize()).toBe(2);
expect(getPaginationEl(0)).not.toHaveClass('disabled');
expect(getPaginationEl(-1)).toHaveClass('disabled');
});
});
describe('when `page` is not a number', function() {
it('handles string', function() {
updateCurrentPage('1');
expect(getPaginationEl(0)).toHaveClass('disabled');
updateCurrentPage('05');
expect(getPaginationEl(-1)).toHaveClass('disabled');
});
});
describe('`num-pages`', function() {
beforeEach(function() {
$rootScope.numpg = null;
element = $compile('
')($rootScope);
$rootScope.$digest();
});
it('equals to total number of pages', function() {
expect($rootScope.numpg).toBe(5);
});
});
describe('setting `pagerConfig`', function() {
var originalConfig = {};
beforeEach(inject(function(uibPagerConfig) {
angular.extend(originalConfig, uibPagerConfig);
uibPagerConfig.previousText = 'PR';
uibPagerConfig.nextText = 'NE';
uibPagerConfig.align = false;
element = $compile('
')($rootScope);
$rootScope.$digest();
}));
afterEach(inject(function(uibPagerConfig) {
// return it to the original state
angular.extend(uibPagerConfig, originalConfig);
}));
it('should change paging text', function() {
expect(getPaginationEl(0).text()).toBe('PR');
expect(getPaginationEl(-1).text()).toBe('NE');
});
it('should not align previous & next page link', function() {
expect(getPaginationEl(0)).not.toHaveClass('previous');
expect(getPaginationEl(-1)).not.toHaveClass('next');
});
});
describe('override configuration from attributes', function() {
beforeEach(function() {
element = $compile('
')($rootScope);
$rootScope.$digest();
});
it('contains 2 li elements', function() {
expect(getPaginationBarSize()).toBe(2);
});
it('should change paging text from attributes', function() {
expect(getPaginationEl(0).text()).toBe('<');
expect(getPaginationEl(-1).text()).toBe('>');
});
it('should not align previous & next page link', function() {
expect(getPaginationEl(0)).not.toHaveClass('previous');
expect(getPaginationEl(-1)).not.toHaveClass('next');
});
it('changes "previous" & "next" text from interpolated attributes', function() {
$rootScope.previousText = '<<';
$rootScope.nextText = '>>';
element = $compile('
')($rootScope);
$rootScope.$digest();
expect(getPaginationEl(0).text()).toBe('<<');
expect(getPaginationEl(-1).text()).toBe('>>');
});
});
it('disables the component when ng-disabled is true', function() {
$rootScope.disable = true;
element = $compile('
')($rootScope);
$rootScope.$digest();
updateCurrentPage(2);
expect(getPaginationEl(0)).toHaveClass('disabled');
expect(getPaginationEl(-1)).toHaveClass('disabled');
$rootScope.disable = false;
$rootScope.$digest();
expect(getPaginationEl(0)).not.toHaveClass('disabled');
expect(getPaginationEl(-1)).not.toHaveClass('disabled');
$rootScope.disable = true;
$rootScope.$digest();
expect(getPaginationEl(0)).toHaveClass('disabled');
expect(getPaginationEl(-1)).toHaveClass('disabled');
});
});
================================================
FILE: src/pagination/docs/demo.html
================================================
Default
The selected page no: {{currentPage}}
Limit the maximum visible buttons
rotate defaulted to true:
rotate defaulted to true and force-ellipses set to true:
rotate set to false:
boundary-link-numbers set to true and rotate defaulted to true:
boundary-link-numbers set to true and rotate set to false:
Page: {{bigCurrentPage}} / {{numPages}}
================================================
FILE: src/pagination/docs/demo.js
================================================
angular.module('ui.bootstrap.demo').controller('PaginationDemoCtrl', function ($scope, $log) {
$scope.totalItems = 64;
$scope.currentPage = 4;
$scope.setPage = function (pageNo) {
$scope.currentPage = pageNo;
};
$scope.pageChanged = function() {
$log.log('Page changed to: ' + $scope.currentPage);
};
$scope.maxSize = 5;
$scope.bigTotalItems = 175;
$scope.bigCurrentPage = 1;
});
================================================
FILE: src/pagination/docs/readme.md
================================================
A lightweight pagination directive that is focused on ... providing pagination & will take care of visualising a pagination bar and enable / disable buttons correctly!
### uib-pagination settings
* `boundary-links`
C
_(Default: `false`)_ -
Whether to display First / Last buttons.
* `boundary-link-numbers`
$
C
_(Default: `false`)_ -
Whether to always display the first and last page numbers. If `max-size` is smaller than the number of pages, then the first and last page numbers are still shown with ellipses in-between as necessary. NOTE: `max-size` refers to the center of the range. This option may add up to 2 more numbers on each side of the displayed range for the end value and what would be an ellipsis but is replaced by a number because it is sequential.
* `direction-links`
$
C
_(Default: `true`)_ -
Whether to display Previous / Next buttons.
* `first-text`
C
_(Default: `First`)_ -
Text for First button.
* `force-ellipses`
$
C
_(Default: `false`)_ -
Also displays ellipses when `rotate` is true and `max-size` is smaller than the number of pages.
* `items-per-page`
$
C
_(Default: `10`)_ -
Maximum number of items per page. A value less than one indicates all items on one page.
* `last-text`
C
_(Default: `Last`)_ -
Text for Last button.
* `max-size`
$
_(Default: `null`)_ -
Limit number for pagination size.
* `next-text`
C
_(Default: `Next`)_ -
Text for Next button.
* `ng-change`
$ -
This can be used to call a function whenever the page changes.
* `ng-disabled`
$
_(Default: `false`)_ -
Used to disable the pagination component.
* `ng-model`
$
-
Current page number. First page is 1.
* `num-pages`
$
readonly
_(Default: `angular.noop`)_ -
An optional expression assigned the total number of pages to display.
* `page-label`
_(Default: `angular.identity`)_ -
An optional expression to override the page label based on passing the current page indexes. Supports page number with `$page` in the template.
* `previous-text`
C
_(Default: `Previous`)_ -
Text for Previous button.
* `rotate`
$
C
_(Default: `true`)_ -
Whether to keep current page in the middle of the visible ones.
* `template-url`
_(Default: `uib/template/pagination/pagination.html`)_ -
Override the template for the component with a custom provided template
* `total-items`
$
-
Total number of items in all pages.
================================================
FILE: src/pagination/index.js
================================================
require('../paging');
require('../tabindex');
require('../../template/pagination/pagination.html.js');
require('./pagination');
var MODULE_NAME = 'ui.bootstrap.module.pagination';
angular.module(MODULE_NAME, ['ui.bootstrap.pagination', 'uib/template/pagination/pagination.html']);
module.exports = MODULE_NAME;
================================================
FILE: src/pagination/pagination.js
================================================
angular.module('ui.bootstrap.pagination', ['ui.bootstrap.paging', 'ui.bootstrap.tabindex'])
.controller('UibPaginationController', ['$scope', '$attrs', '$parse', 'uibPaging', 'uibPaginationConfig', function($scope, $attrs, $parse, uibPaging, uibPaginationConfig) {
var ctrl = this;
// Setup configuration parameters
var maxSize = angular.isDefined($attrs.maxSize) ? $scope.$parent.$eval($attrs.maxSize) : uibPaginationConfig.maxSize,
rotate = angular.isDefined($attrs.rotate) ? $scope.$parent.$eval($attrs.rotate) : uibPaginationConfig.rotate,
forceEllipses = angular.isDefined($attrs.forceEllipses) ? $scope.$parent.$eval($attrs.forceEllipses) : uibPaginationConfig.forceEllipses,
boundaryLinkNumbers = angular.isDefined($attrs.boundaryLinkNumbers) ? $scope.$parent.$eval($attrs.boundaryLinkNumbers) : uibPaginationConfig.boundaryLinkNumbers,
pageLabel = angular.isDefined($attrs.pageLabel) ? function(idx) { return $scope.$parent.$eval($attrs.pageLabel, {$page: idx}); } : angular.identity;
$scope.boundaryLinks = angular.isDefined($attrs.boundaryLinks) ? $scope.$parent.$eval($attrs.boundaryLinks) : uibPaginationConfig.boundaryLinks;
$scope.directionLinks = angular.isDefined($attrs.directionLinks) ? $scope.$parent.$eval($attrs.directionLinks) : uibPaginationConfig.directionLinks;
$attrs.$set('role', 'menu');
uibPaging.create(this, $scope, $attrs);
if ($attrs.maxSize) {
ctrl._watchers.push($scope.$parent.$watch($parse($attrs.maxSize), function(value) {
maxSize = parseInt(value, 10);
ctrl.render();
}));
}
// Create page object used in template
function makePage(number, text, isActive) {
return {
number: number,
text: text,
active: isActive
};
}
function getPages(currentPage, totalPages) {
var pages = [];
// Default page limits
var startPage = 1, endPage = totalPages;
var isMaxSized = angular.isDefined(maxSize) && maxSize < totalPages;
// recompute if maxSize
if (isMaxSized) {
if (rotate) {
// Current page is displayed in the middle of the visible ones
startPage = Math.max(currentPage - Math.floor(maxSize / 2), 1);
endPage = startPage + maxSize - 1;
// Adjust if limit is exceeded
if (endPage > totalPages) {
endPage = totalPages;
startPage = endPage - maxSize + 1;
}
} else {
// Visible pages are paginated with maxSize
startPage = (Math.ceil(currentPage / maxSize) - 1) * maxSize + 1;
// Adjust last page if limit is exceeded
endPage = Math.min(startPage + maxSize - 1, totalPages);
}
}
// Add page number links
for (var number = startPage; number <= endPage; number++) {
var page = makePage(number, pageLabel(number), number === currentPage);
pages.push(page);
}
// Add links to move between page sets
if (isMaxSized && maxSize > 0 && (!rotate || forceEllipses || boundaryLinkNumbers)) {
if (startPage > 1) {
if (!boundaryLinkNumbers || startPage > 3) { //need ellipsis for all options unless range is too close to beginning
var previousPageSet = makePage(startPage - 1, '...', false);
pages.unshift(previousPageSet);
}
if (boundaryLinkNumbers) {
if (startPage === 3) { //need to replace ellipsis when the buttons would be sequential
var secondPageLink = makePage(2, '2', false);
pages.unshift(secondPageLink);
}
//add the first page
var firstPageLink = makePage(1, '1', false);
pages.unshift(firstPageLink);
}
}
if (endPage < totalPages) {
if (!boundaryLinkNumbers || endPage < totalPages - 2) { //need ellipsis for all options unless range is too close to end
var nextPageSet = makePage(endPage + 1, '...', false);
pages.push(nextPageSet);
}
if (boundaryLinkNumbers) {
if (endPage === totalPages - 2) { //need to replace ellipsis when the buttons would be sequential
var secondToLastPageLink = makePage(totalPages - 1, totalPages - 1, false);
pages.push(secondToLastPageLink);
}
//add the last page
var lastPageLink = makePage(totalPages, totalPages, false);
pages.push(lastPageLink);
}
}
}
return pages;
}
var originalRender = this.render;
this.render = function() {
originalRender();
if ($scope.page > 0 && $scope.page <= $scope.totalPages) {
$scope.pages = getPages($scope.page, $scope.totalPages);
}
};
}])
.constant('uibPaginationConfig', {
itemsPerPage: 10,
boundaryLinks: false,
boundaryLinkNumbers: false,
directionLinks: true,
firstText: 'First',
previousText: 'Previous',
nextText: 'Next',
lastText: 'Last',
rotate: true,
forceEllipses: false
})
.directive('uibPagination', ['$parse', 'uibPaginationConfig', function($parse, uibPaginationConfig) {
return {
scope: {
totalItems: '=',
firstText: '@',
previousText: '@',
nextText: '@',
lastText: '@',
ngDisabled:'='
},
require: ['uibPagination', '?ngModel'],
restrict: 'A',
controller: 'UibPaginationController',
controllerAs: 'pagination',
templateUrl: function(element, attrs) {
return attrs.templateUrl || 'uib/template/pagination/pagination.html';
},
link: function(scope, element, attrs, ctrls) {
element.addClass('pagination');
var paginationCtrl = ctrls[0], ngModelCtrl = ctrls[1];
if (!ngModelCtrl) {
return; // do nothing if no ng-model
}
paginationCtrl.init(ngModelCtrl, uibPaginationConfig);
}
};
}]);
================================================
FILE: src/pagination/test/pagination.spec.js
================================================
describe('pagination directive', function() {
var $compile, $rootScope, $document, $templateCache, body, element;
beforeEach(module('ui.bootstrap.pagination'));
beforeEach(module('uib/template/pagination/pagination.html'));
beforeEach(inject(function(_$compile_, _$rootScope_, _$document_, _$templateCache_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
$rootScope.total = 47; // 5 pages
$rootScope.currentPage = 3;
$rootScope.disabled = false;
$document = _$document_;
$templateCache = _$templateCache_;
body = $document.find('body');
element = $compile('
')($rootScope);
$rootScope.$digest();
}));
function getPaginationBarSize() {
return element.find('li').length;
}
function getPaginationEl(index) {
return element.find('li').eq(index);
}
// Returns a comma-separated string that represents the pager, like: "Prev, 1, 2, 3, Next"
function getPaginationAsText() {
var len = getPaginationBarSize(), outItems = [];
for (var i = 0; i < len; i++) {
outItems.push(getPaginationEl(i).text());
}
return outItems.join(', ');
}
function clickPaginationEl(index) {
getPaginationEl(index).find('a').click();
}
function getPaginationLinkEl(elem, index) {
return elem.find('li').eq(index).find('a');
}
function updateCurrentPage(value) {
$rootScope.currentPage = value;
$rootScope.$digest();
}
function setDisabled(value) {
$rootScope.disabled = value;
$rootScope.$digest();
}
it('has a "pagination" css class', function() {
expect(element.hasClass('pagination')).toBe(true);
});
it('has accessibility attributes', function() {
expect(element.attr('role')).toEqual('menu');
var li = element.find('li');
for (var i = 0; i < li.length; i++) {
expect(li.eq(i).attr('role')).toEqual('menuitem');
}
});
it('exposes the controller to the template', function() {
$templateCache.put('uib/template/pagination/pagination.html', '
{{pagination.randomText}}
');
var scope = $rootScope.$new();
element = $compile('
')(scope);
$rootScope.$digest();
var ctrl = element.controller('uibPagination');
expect(ctrl).toBeDefined();
ctrl.randomText = 'foo';
$rootScope.$digest();
expect(element.html()).toBe('
foo
');
});
it('allows custom templates', function() {
$templateCache.put('foo/bar.html', '
baz
');
var scope = $rootScope.$new();
element = $compile('
')(scope);
$rootScope.$digest();
expect(element.html()).toBe('
baz
');
});
it('contains num-pages + 2 li elements', function() {
expect(getPaginationBarSize()).toBe(7);
expect(getPaginationEl(0).text()).toBe('Previous');
expect(getPaginationEl(-1).text()).toBe('Next');
});
it('has the number of the page as text in each page item', function() {
for (var i = 1; i <= 5; i++) {
expect(getPaginationEl(i).text()).toEqual(''+i);
}
});
it('sets the current page to be active', function() {
expect(getPaginationEl($rootScope.currentPage).hasClass('active')).toBe(true);
});
it('disables the "previous" link if current page is 1', function() {
updateCurrentPage(1);
expect(getPaginationEl(0).hasClass('disabled')).toBe(true);
});
it('disables the "next" link if current page is last', function() {
updateCurrentPage(5);
expect(getPaginationEl(-1).hasClass('disabled')).toBe(true);
});
it('changes currentPage if a page link is clicked', function() {
clickPaginationEl(2);
expect($rootScope.currentPage).toBe(2);
});
it('changes currentPage if the "previous" link is clicked', function() {
clickPaginationEl(0);
expect($rootScope.currentPage).toBe(2);
});
it('changes currentPage if the "next" link is clicked', function() {
clickPaginationEl(-1);
expect($rootScope.currentPage).toBe(4);
});
it('does not change the current page on "previous" click if already at first page', function() {
updateCurrentPage(1);
clickPaginationEl(0);
expect($rootScope.currentPage).toBe(1);
});
it('does not change the current page on "next" click if already at last page', function() {
updateCurrentPage(5);
clickPaginationEl(-1);
expect($rootScope.currentPage).toBe(5);
});
it('changes the number of pages when `total-items` changes', function() {
$rootScope.total = 78; // 8 pages
$rootScope.$digest();
expect(getPaginationBarSize()).toBe(10);
expect(getPaginationEl(0).text()).toBe('Previous');
expect(getPaginationEl(-1).text()).toBe('Next');
});
it('does not "break" when `total-items` is undefined', function() {
$rootScope.total = undefined;
$rootScope.$digest();
expect(getPaginationBarSize()).toBe(3); // Previous, 1, Next
expect(getPaginationEl(0)).toHaveClass('disabled');
expect(getPaginationEl(1)).toHaveClass('active');
expect(getPaginationEl(2)).toHaveClass('disabled');
});
it('does not "break" when `total-items` is negative', function() {
$rootScope.total = -1;
$rootScope.$digest();
expect(getPaginationBarSize()).toBe(3); // Previous, 1, Next
expect(getPaginationEl(0)).toHaveClass('disabled');
expect(getPaginationEl(1)).toHaveClass('active');
expect(getPaginationEl(2)).toHaveClass('disabled');
});
it('does not change the current page when `total-items` changes but is valid', function() {
$rootScope.currentPage = 1;
$rootScope.total = 18; // 2 pages
$rootScope.$digest();
expect($rootScope.currentPage).toBe(1);
});
it('should blur a page link after it has been clicked', function() {
body.append(element);
var linkEl = getPaginationLinkEl(element, 2);
linkEl.focus();
expect(linkEl).toHaveFocus();
linkEl.click();
expect(linkEl).not.toHaveFocus();
element.remove();
});
it('should blur the "next" link after it has been clicked', function() {
body.append(element);
var linkEl = getPaginationLinkEl(element, -1);
linkEl.focus();
expect(linkEl).toHaveFocus();
linkEl.click();
expect(linkEl).not.toHaveFocus();
element.remove();
});
it('should blur the "prev" link after it has been clicked', function() {
body.append(element);
var linkEl = getPaginationLinkEl(element, 0);
linkEl.focus();
expect(linkEl).toHaveFocus();
linkEl.click();
expect(linkEl).not.toHaveFocus();
element.remove();
});
describe('`items-per-page`', function() {
beforeEach(function() {
$rootScope.perpage = 5;
element = $compile('
')($rootScope);
$rootScope.$digest();
});
it('changes the number of pages', function() {
expect(getPaginationBarSize()).toBe(12);
expect(getPaginationEl(0).text()).toBe('Previous');
expect(getPaginationEl(-1).text()).toBe('Next');
});
it('changes the number of pages when changes', function() {
$rootScope.perpage = 20;
$rootScope.$digest();
expect(getPaginationBarSize()).toBe(5);
expect(getPaginationEl(0).text()).toBe('Previous');
expect(getPaginationEl(-1).text()).toBe('Next');
});
it('selects the last page when current page is too big', function() {
$rootScope.perpage = 30;
$rootScope.$digest();
expect($rootScope.currentPage).toBe(2);
expect(getPaginationBarSize()).toBe(4);
expect(getPaginationEl(0).text()).toBe('Previous');
expect(getPaginationEl(-1).text()).toBe('Next');
});
it('displays a single page when it is negative', function() {
$rootScope.perpage = -1;
$rootScope.$digest();
expect(getPaginationBarSize()).toBe(3);
expect(getPaginationEl(0).text()).toBe('Previous');
expect(getPaginationEl(1).text()).toBe('1');
expect(getPaginationEl(-1).text()).toBe('Next');
});
});
describe('executes `ng-change` expression', function() {
beforeEach(function() {
$rootScope.selectPageHandler = jasmine.createSpy('selectPageHandler');
element = $compile('
')($rootScope);
$rootScope.$digest();
});
it('when an element is clicked', function() {
clickPaginationEl(2);
expect($rootScope.selectPageHandler).toHaveBeenCalled();
});
});
describe('when `page` is not a number', function() {
it('handles numerical string', function() {
updateCurrentPage('2');
expect(getPaginationEl(2)).toHaveClass('active');
updateCurrentPage('04');
expect(getPaginationEl(4)).toHaveClass('active');
});
it('defaults to 1 if non-numeric', function() {
updateCurrentPage('pizza');
expect(getPaginationEl(1)).toHaveClass('active');
});
});
describe('with `max-size` option', function() {
beforeEach(function() {
$rootScope.total = 98; // 10 pages
$rootScope.currentPage = 3;
$rootScope.maxSize = 5;
element = $compile('
')($rootScope);
$rootScope.$digest();
});
it('contains maxsize + 2 li elements', function() {
expect(getPaginationBarSize()).toBe($rootScope.maxSize + 2);
expect(getPaginationEl(0).text()).toBe('Previous');
expect(getPaginationEl(-1).text()).toBe('Next');
});
it('shows the page number even if it can\'t be shown in the middle', function() {
updateCurrentPage(1);
expect(getPaginationEl(1)).toHaveClass('active');
updateCurrentPage(10);
expect(getPaginationEl(-2)).toHaveClass('active');
});
it('shows the page number in middle after the next link is clicked', function() {
updateCurrentPage(6);
clickPaginationEl(-1);
expect($rootScope.currentPage).toBe(7);
expect(getPaginationEl(3)).toHaveClass('active');
expect(getPaginationEl(3).text()).toBe(''+$rootScope.currentPage);
});
it('shows the page number in middle after the prev link is clicked', function() {
updateCurrentPage(7);
clickPaginationEl(0);
expect($rootScope.currentPage).toBe(6);
expect(getPaginationEl(3)).toHaveClass('active');
expect(getPaginationEl(3).text()).toBe(''+$rootScope.currentPage);
});
it('changes pagination bar size when max-size value changed', function() {
$rootScope.maxSize = 7;
$rootScope.$digest();
expect(getPaginationBarSize()).toBe(9);
});
it('sets the pagination bar size to num-pages, if max-size is greater than num-pages ', function() {
$rootScope.maxSize = 15;
$rootScope.$digest();
expect(getPaginationBarSize()).toBe(12);
});
it('should not change value of max-size expression, if max-size is greater than num-pages ', function() {
$rootScope.maxSize = 15;
$rootScope.$digest();
expect($rootScope.maxSize).toBe(15);
});
it('should not display page numbers, if max-size is zero', function() {
$rootScope.maxSize = 0;
$rootScope.$digest();
expect(getPaginationBarSize()).toBe(2);
expect(getPaginationEl(0).text()).toBe('Previous');
expect(getPaginationEl(-1).text()).toBe('Next');
});
it('should blur page link when visible range changes', function () {
body.append(element);
var linkEl = getPaginationLinkEl(element, 4);
linkEl.focus();
expect(linkEl).toHaveFocus();
linkEl.click();
expect(linkEl).not.toHaveFocus();
element.remove();
});
});
describe('with `force-ellipses` option', function() {
beforeEach(function() {
$rootScope.total = 98; // 10 pages
$rootScope.currentPage = 3;
$rootScope.maxSize = 5;
element = $compile('
')($rootScope);
$rootScope.$digest();
});
it('contains maxsize + 3 li elements', function() {
expect(getPaginationBarSize()).toBe($rootScope.maxSize + 3);
expect(getPaginationEl(0).text()).toBe('Previous');
expect(getPaginationEl(-1).text()).toBe('Next');
expect(getPaginationEl(-2).text()).toBe('...');
});
it('shows the page number in middle after the next link is clicked', function() {
updateCurrentPage(6);
clickPaginationEl(-1);
expect($rootScope.currentPage).toBe(7);
expect(getPaginationEl(4)).toHaveClass('active');
expect(getPaginationEl(4).text()).toBe(''+$rootScope.currentPage);
});
it('shows the page number in middle after the prev link is clicked', function() {
updateCurrentPage(7);
clickPaginationEl(0);
expect($rootScope.currentPage).toBe(6);
expect(getPaginationEl(4)).toHaveClass('active');
expect(getPaginationEl(4).text()).toBe(''+$rootScope.currentPage);
});
it('changes pagination bar size when max-size value changed', function() {
$rootScope.maxSize = 7;
$rootScope.$digest();
expect(getPaginationBarSize()).toBe(10);
});
it('should display an ellipsis on the right if the last displayed page\'s number is less than the last page', function() {
updateCurrentPage(1);
expect(getPaginationAsText()).toBe('Previous, 1, 2, 3, 4, 5, ..., Next');
});
it('should display an ellipsis on the left if the first displayed page\'s number is greater than 1', function() {
updateCurrentPage(10);
expect(getPaginationAsText()).toBe('Previous, ..., 6, 7, 8, 9, 10, Next');
});
it('should display both ellipsis\' if the displayed range is in the middle', function() {
updateCurrentPage(5);
expect(getPaginationAsText()).toBe('Previous, ..., 3, 4, 5, 6, 7, ..., Next');
});
it('should not display any ellipses if the number of pages >= maxsize', function() {
$rootScope.maxSize = 10;
$rootScope.$digest();
expect(getPaginationAsText()).toBe('Previous, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, Next');
});
});
describe('with `boundary-link-numbers` option', function() {
beforeEach(function() {
$rootScope.total = 98; // 10 pages
$rootScope.currentPage = 3;
$rootScope.maxSize = 5;
element = $compile('
')($rootScope);
$rootScope.$digest();
});
it('contains maxsize + 4 li elements', function() {
expect(getPaginationBarSize()).toBe($rootScope.maxSize + 4);
expect(getPaginationEl(0).text()).toBe('Previous');
expect(getPaginationEl(-1).text()).toBe('Next');
expect(getPaginationEl(-2).text()).toBe('10');
expect(getPaginationEl(-3).text()).toBe('...');
});
it('shows the page number in middle after the next link is clicked', function() {
updateCurrentPage(6);
clickPaginationEl(-1);
expect($rootScope.currentPage).toBe(7);
expect(getPaginationEl(5)).toHaveClass('active');
expect(getPaginationEl(5).text()).toBe(''+$rootScope.currentPage);
});
it('shows the page number in middle after the prev link is clicked', function() {
updateCurrentPage(7);
clickPaginationEl(0);
expect($rootScope.currentPage).toBe(6);
expect(getPaginationEl(5)).toHaveClass('active');
expect(getPaginationEl(5).text()).toBe(''+$rootScope.currentPage);
});
it('changes pagination bar size when max-size value changed', function() {
$rootScope.maxSize = 7;
$rootScope.$digest();
expect(getPaginationBarSize()).toBe(11);
});
it('should display an ellipsis on the right if the last displayed page\'s number is less than the last page', function() {
updateCurrentPage(1);
expect(getPaginationAsText()).toBe('Previous, 1, 2, 3, 4, 5, ..., 10, Next');
});
it('should display an ellipsis on the left if the first displayed page\'s number is greater than 1', function() {
updateCurrentPage(10);
expect(getPaginationAsText()).toBe('Previous, 1, ..., 6, 7, 8, 9, 10, Next');
});
it('should display both ellipses if the displayed range is in the middle', function() {
$rootScope.maxSize = 3;
$rootScope.$digest();
updateCurrentPage(6);
expect(getPaginationAsText()).toBe('Previous, 1, ..., 5, 6, 7, ..., 10, Next');
});
it('should not display any ellipses if the number of pages >= maxsize', function() {
$rootScope.maxSize = 10;
$rootScope.$digest();
expect(getPaginationAsText()).toBe('Previous, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, Next');
});
it('should not display an ellipsis on the left if the start page is 2', function() {
updateCurrentPage(4);
expect(getPaginationAsText()).toBe('Previous, 1, 2, 3, 4, 5, 6, ..., 10, Next');
});
it('should not display an ellipsis on the left if the start page is 3', function() {
updateCurrentPage(5);
expect(getPaginationAsText()).toBe('Previous, 1, 2, 3, 4, 5, 6, 7, ..., 10, Next');
});
it('should not display an ellipsis on the right if the end page is totalPages - 1', function() {
updateCurrentPage(7);
expect(getPaginationAsText()).toBe('Previous, 1, ..., 5, 6, 7, 8, 9, 10, Next');
});
it('should not display an ellipsis on the right if the end page is totalPages - 2', function() {
updateCurrentPage(6);
expect(getPaginationAsText()).toBe('Previous, 1, ..., 4, 5, 6, 7, 8, 9, 10, Next');
});
it('should not display any ellipses if the number of pages <= maxsize + 4 and current page is in center', function() {
$rootScope.total = 88; // 9 pages
$rootScope.$digest();
updateCurrentPage(5);
expect(getPaginationAsText()).toBe('Previous, 1, 2, 3, 4, 5, 6, 7, 8, 9, Next');
});
});
describe('with `max-size` option & no `rotate`', function() {
beforeEach(function() {
$rootScope.total = 115; // 12 pages
$rootScope.currentPage = 7;
$rootScope.maxSize = 5;
$rootScope.rotate = false;
element = $compile('
')($rootScope);
$rootScope.$digest();
});
it('contains maxsize + 4 elements', function() {
expect(getPaginationBarSize()).toBe($rootScope.maxSize + 4);
expect(getPaginationEl(0).text()).toBe('Previous');
expect(getPaginationEl(1).text()).toBe('...');
expect(getPaginationEl(2).text()).toBe('6');
expect(getPaginationEl(-3).text()).toBe('10');
expect(getPaginationEl(-2).text()).toBe('...');
expect(getPaginationEl(-1).text()).toBe('Next');
});
it('shows only the next ellipsis element on first page set', function() {
updateCurrentPage(3);
expect(getPaginationEl(1).text()).toBe('1');
expect(getPaginationEl(-3).text()).toBe('5');
expect(getPaginationEl(-2).text()).toBe('...');
});
it('shows only the previous ellipsis element on last page set', function() {
updateCurrentPage(12);
expect(getPaginationBarSize()).toBe(5);
expect(getPaginationEl(1).text()).toBe('...');
expect(getPaginationEl(2).text()).toBe('11');
expect(getPaginationEl(-2).text()).toBe('12');
});
it('moves to the previous set when first ellipsis is clicked', function() {
expect(getPaginationEl(1).text()).toBe('...');
clickPaginationEl(1);
expect($rootScope.currentPage).toBe(5);
expect(getPaginationEl(-3)).toHaveClass('active');
});
it('moves to the next set when last ellipsis is clicked', function() {
expect(getPaginationEl(-2).text()).toBe('...');
clickPaginationEl(-2);
expect($rootScope.currentPage).toBe(11);
expect(getPaginationEl(2)).toHaveClass('active');
});
it('should not display page numbers, if max-size is zero', function() {
$rootScope.maxSize = 0;
$rootScope.$digest();
expect(getPaginationBarSize()).toBe(2);
expect(getPaginationEl(0).text()).toBe('Previous');
expect(getPaginationEl(1).text()).toBe('Next');
});
});
describe('pagination directive with `boundary-links`', function() {
beforeEach(function() {
element = $compile('
')($rootScope);
$rootScope.$digest();
});
it('contains num-pages + 4 li elements', function() {
expect(getPaginationBarSize()).toBe(9);
expect(getPaginationEl(0).text()).toBe('First');
expect(getPaginationEl(1).text()).toBe('Previous');
expect(getPaginationEl(-2).text()).toBe('Next');
expect(getPaginationEl(-1).text()).toBe('Last');
});
it('has first and last li elements visible', function() {
expect(getPaginationEl(0).css('display')).not.toBe('none');
expect(getPaginationEl(-1).css('display')).not.toBe('none');
});
it('disables the "first" & "previous" link if current page is 1', function() {
updateCurrentPage(1);
expect(getPaginationEl(0)).toHaveClass('disabled');
expect(getPaginationEl(1)).toHaveClass('disabled');
});
it('disables the "last" & "next" link if current page is num-pages', function() {
updateCurrentPage(5);
expect(getPaginationEl(-2)).toHaveClass('disabled');
expect(getPaginationEl(-1)).toHaveClass('disabled');
});
it('changes currentPage if the "first" link is clicked', function() {
clickPaginationEl(0);
expect($rootScope.currentPage).toBe(1);
});
it('changes currentPage if the "last" link is clicked', function() {
clickPaginationEl(-1);
expect($rootScope.currentPage).toBe(5);
});
it('does not change the current page on "first" click if already at first page', function() {
updateCurrentPage(1);
clickPaginationEl(0);
expect($rootScope.currentPage).toBe(1);
});
it('does not change the current page on "last" click if already at last page', function() {
updateCurrentPage(5);
clickPaginationEl(-1);
expect($rootScope.currentPage).toBe(5);
});
it('changes "first" & "last" text from attributes', function() {
element = $compile('
')($rootScope);
$rootScope.$digest();
expect(getPaginationEl(0).text()).toBe('<<<');
expect(getPaginationEl(-1).text()).toBe('>>>');
});
it('changes "previous" & "next" text from attributes', function() {
element = $compile('
')($rootScope);
$rootScope.$digest();
expect(getPaginationEl(1).text()).toBe('<<');
expect(getPaginationEl(-2).text()).toBe('>>');
});
it('changes "first" & "last" text from interpolated attributes', function() {
$rootScope.myfirstText = '<<<';
$rootScope.mylastText = '>>>';
element = $compile('
')($rootScope);
$rootScope.$digest();
expect(getPaginationEl(0).text()).toBe('<<<');
expect(getPaginationEl(-1).text()).toBe('>>>');
});
it('changes "previous" & "next" text from interpolated attributes', function() {
$rootScope.previousText = '<<';
$rootScope.nextText = '>>';
element = $compile('
')($rootScope);
$rootScope.$digest();
expect(getPaginationEl(1).text()).toBe('<<');
expect(getPaginationEl(-2).text()).toBe('>>');
});
it('should blur the "first" link after it has been clicked', function() {
body.append(element);
var linkEl = getPaginationLinkEl(element, 0);
linkEl.focus();
expect(linkEl).toHaveFocus();
linkEl.click();
expect(linkEl).not.toHaveFocus();
element.remove();
});
it('should blur the "last" link after it has been clicked', function() {
body.append(element);
var linkEl = getPaginationLinkEl(element, -1);
linkEl.focus();
expect(linkEl).toHaveFocus();
linkEl.click();
expect(linkEl).not.toHaveFocus();
element.remove();
});
});
describe('pagination directive with just number links', function() {
beforeEach(function() {
element = $compile('
')($rootScope);
$rootScope.$digest();
});
it('contains num-pages li elements', function() {
expect(getPaginationBarSize()).toBe(5);
expect(getPaginationEl(0).text()).toBe('1');
expect(getPaginationEl(-1).text()).toBe('5');
});
it('has the number of the page as text in each page item', function() {
for(var i = 0; i < 5; i++) {
expect(getPaginationEl(i).text()).toEqual(''+(i+1));
}
});
it('sets the current page to be active', function() {
expect(getPaginationEl(2)).toHaveClass('active');
});
it('does not disable the "1" link if current page is 1', function() {
updateCurrentPage(1);
expect(getPaginationEl(0)).not.toHaveClass('disabled');
expect(getPaginationEl(0)).toHaveClass('active');
});
it('does not disable the "last" link if current page is last page', function() {
updateCurrentPage(5);
expect(getPaginationEl(-1)).not.toHaveClass('disabled');
expect(getPaginationEl(-1)).toHaveClass('active');
});
it('changes currentPage if a page link is clicked', function() {
clickPaginationEl(1);
expect($rootScope.currentPage).toBe(2);
});
it('changes the number of items when total items changes', function() {
$rootScope.total = 73; // 8 pages
$rootScope.$digest();
expect(getPaginationBarSize()).toBe(8);
expect(getPaginationEl(0).text()).toBe('1');
expect(getPaginationEl(-1).text()).toBe('8');
});
});
describe('with just boundary & number links', function() {
beforeEach(function() {
$rootScope.directions = false;
element = $compile('
')($rootScope);
$rootScope.$digest();
});
it('contains number of pages + 2 li elements', function() {
expect(getPaginationBarSize()).toBe(7);
expect(getPaginationEl(0).text()).toBe('First');
expect(getPaginationEl(1).text()).toBe('1');
expect(getPaginationEl(-2).text()).toBe('5');
expect(getPaginationEl(-1).text()).toBe('Last');
});
it('disables the "first" & activates "1" link if current page is 1', function() {
updateCurrentPage(1);
expect(getPaginationEl(0)).toHaveClass('disabled');
expect(getPaginationEl(1)).not.toHaveClass('disabled');
expect(getPaginationEl(1)).toHaveClass('active');
});
it('disables the "last" & "next" link if current page is num-pages', function() {
updateCurrentPage(5);
expect(getPaginationEl(-2)).toHaveClass('active');
expect(getPaginationEl(-2)).not.toHaveClass('disabled');
expect(getPaginationEl(-1)).toHaveClass('disabled');
});
});
describe('`num-pages`', function() {
beforeEach(function() {
$rootScope.numpg = null;
element = $compile('
')($rootScope);
$rootScope.$digest();
});
it('equals to total number of pages', function() {
expect($rootScope.numpg).toBe(5);
});
it('changes when total number of pages change', function() {
$rootScope.total = 73; // 8 pages
$rootScope.$digest();
expect($rootScope.numpg).toBe(8);
});
it('shows minimun one page if total items are not defined and does not break binding', function() {
$rootScope.total = undefined;
$rootScope.$digest();
expect($rootScope.numpg).toBe(1);
$rootScope.total = 73; // 8 pages
$rootScope.$digest();
expect($rootScope.numpg).toBe(8);
});
});
describe('setting `paginationConfig`', function() {
var originalConfig, paginationConfig;
beforeEach(inject(function(_uibPaginationConfig_) {
originalConfig = angular.copy(_uibPaginationConfig_);
paginationConfig = _uibPaginationConfig_;
}));
afterEach(inject(function(_uibPaginationConfig_) {
// return it to the original stat
angular.copy(originalConfig, _uibPaginationConfig_);
}));
it('should change paging text', function() {
paginationConfig.boundaryLinks = true;
paginationConfig.directionLinks = true;
paginationConfig.firstText = 'FI';
paginationConfig.previousText = 'PR';
paginationConfig.nextText = 'NE';
paginationConfig.lastText = 'LA';
element = $compile('
')($rootScope);
$rootScope.$digest();
expect(getPaginationEl(0).text()).toBe('FI');
expect(getPaginationEl(1).text()).toBe('PR');
expect(getPaginationEl(-2).text()).toBe('NE');
expect(getPaginationEl(-1).text()).toBe('LA');
});
it('contains number of pages + 2 li elements', function() {
paginationConfig.itemsPerPage = 5;
element = $compile('
')($rootScope);
$rootScope.$digest();
expect(getPaginationBarSize()).toBe(12);
});
it('should take maxSize defaults into account', function() {
paginationConfig.maxSize = 2;
element = $compile('
')($rootScope);
$rootScope.$digest();
expect(getPaginationBarSize()).toBe(4);
});
it('should take forceEllipses defaults into account', function () {
paginationConfig.forceEllipses = true;
element = $compile('
')($rootScope);
$rootScope.$digest();
// Should contain 2 nav buttons, 2 pages, and 2 ellipsis since the currentPage defaults to 3, which is in the middle
expect(getPaginationBarSize()).toBe(6);
});
it('should take boundaryLinkNumbers defaults into account', function () {
paginationConfig.boundaryLinkNumbers = true;
$rootScope.total = 88; // 9 pages
$rootScope.currentPage = 5;
element = $compile('
')($rootScope);
$rootScope.$digest();
// Should contain 2 nav buttons, 2 pages, 2 ellipsis, and 2 extra end numbers since the currentPage is in the middle
expect(getPaginationBarSize()).toBe(9);
expect(getPaginationAsText()).toBe('Previous, 1, ..., 4, 5, 6, ..., 9, Next');
});
});
describe('override configuration from attributes', function() {
beforeEach(function() {
$rootScope.pageLabel = function(id) {
return 'test_'+ id;
};
element = $compile('
')($rootScope);
$rootScope.$digest();
});
it('contains number of pages + 4 li elements', function() {
expect(getPaginationBarSize()).toBe(9);
});
it('should change paging text from attribute', function() {
expect(getPaginationEl(0).text()).toBe('<<');
expect(getPaginationEl(1).text()).toBe('<');
expect(getPaginationEl(-2).text()).toBe('>');
expect(getPaginationEl(-1).text()).toBe('>>');
});
it('has the label of the page as text in each page item', function() {
for (var i = 1; i <= 5; i++) {
// +1 because the first element is a <
expect(getPaginationEl(i+1).text()).toEqual('test_'+i);
}
});
});
describe('disabled with ngDisable', function() {
beforeEach(function() {
element = $compile('
')($rootScope);
$rootScope.currentPage = 3;
$rootScope.$digest();
});
it('should not respond to clicking', function() {
setDisabled(true);
clickPaginationEl(2);
expect($rootScope.currentPage).toBe(3);
setDisabled(false);
clickPaginationEl(2);
expect($rootScope.currentPage).toBe(2);
});
it('should change the class of all buttons except selected one', function() {
setDisabled(false);
expect(getPaginationEl(3).hasClass('active')).toBe(true);
expect(getPaginationEl(4).hasClass('active')).toBe(false);
setDisabled(true);
expect(getPaginationEl(3).hasClass('disabled')).toBe(false);
expect(getPaginationEl(4).hasClass('disabled')).toBe(true);
});
});
});
describe('pagination directive', function() {
var $compile, $rootScope, element;
beforeEach(module('ui.bootstrap.pagination'));
beforeEach(module('uib/template/pagination/pagination.html'));
beforeEach(inject(function(_$compile_, _$rootScope_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
}));
it('should retain the model value when total-items starts as undefined', function() {
$rootScope.currentPage = 5;
$rootScope.total = undefined;
element = $compile('
')($rootScope);
$rootScope.$digest();
expect($rootScope.currentPage).toBe(5);
$rootScope.total = 100;
$rootScope.$digest();
expect($rootScope.currentPage).toBe(5);
});
});
================================================
FILE: src/paging/index.js
================================================
require('./paging');
var MODULE_NAME = 'ui.bootstrap.module.paging';
angular.module(MODULE_NAME, ['ui.bootstrap.paging']);
module.exports = MODULE_NAME;
================================================
FILE: src/paging/paging.js
================================================
angular.module('ui.bootstrap.paging', [])
/**
* Helper internal service for generating common controller code between the
* pager and pagination components
*/
.factory('uibPaging', ['$parse', function($parse) {
return {
create: function(ctrl, $scope, $attrs) {
ctrl.setNumPages = $attrs.numPages ? $parse($attrs.numPages).assign : angular.noop;
ctrl.ngModelCtrl = { $setViewValue: angular.noop }; // nullModelCtrl
ctrl._watchers = [];
ctrl.init = function(ngModelCtrl, config) {
ctrl.ngModelCtrl = ngModelCtrl;
ctrl.config = config;
ngModelCtrl.$render = function() {
ctrl.render();
};
if ($attrs.itemsPerPage) {
ctrl._watchers.push($scope.$parent.$watch($attrs.itemsPerPage, function(value) {
ctrl.itemsPerPage = parseInt(value, 10);
$scope.totalPages = ctrl.calculateTotalPages();
ctrl.updatePage();
}));
} else {
ctrl.itemsPerPage = config.itemsPerPage;
}
$scope.$watch('totalItems', function(newTotal, oldTotal) {
if (angular.isDefined(newTotal) || newTotal !== oldTotal) {
$scope.totalPages = ctrl.calculateTotalPages();
ctrl.updatePage();
}
});
};
ctrl.calculateTotalPages = function() {
var totalPages = ctrl.itemsPerPage < 1 ? 1 : Math.ceil($scope.totalItems / ctrl.itemsPerPage);
return Math.max(totalPages || 0, 1);
};
ctrl.render = function() {
$scope.page = parseInt(ctrl.ngModelCtrl.$viewValue, 10) || 1;
};
$scope.selectPage = function(page, evt) {
if (evt) {
evt.preventDefault();
}
var clickAllowed = !$scope.ngDisabled || !evt;
if (clickAllowed && $scope.page !== page && page > 0 && page <= $scope.totalPages) {
if (evt && evt.target) {
evt.target.blur();
}
ctrl.ngModelCtrl.$setViewValue(page);
ctrl.ngModelCtrl.$render();
}
};
$scope.getText = function(key) {
return $scope[key + 'Text'] || ctrl.config[key + 'Text'];
};
$scope.noPrevious = function() {
return $scope.page === 1;
};
$scope.noNext = function() {
return $scope.page === $scope.totalPages;
};
ctrl.updatePage = function() {
ctrl.setNumPages($scope.$parent, $scope.totalPages); // Readonly variable
if ($scope.page > $scope.totalPages) {
$scope.selectPage($scope.totalPages);
} else {
ctrl.ngModelCtrl.$render();
}
};
$scope.$on('$destroy', function() {
while (ctrl._watchers.length) {
ctrl._watchers.shift()();
}
});
}
};
}]);
================================================
FILE: src/paging/test/paging.spec.js
================================================
describe('paging factory', function() {
var $rootScope, $scope, ctrl, attrs;
beforeEach(module('ui.bootstrap.paging'));
beforeEach(inject(function(_$rootScope_, uibPaging) {
$rootScope = _$rootScope_;
$scope = $rootScope.$new();
ctrl = {};
attrs = {};
uibPaging.create(ctrl, $scope, attrs);
}));
describe('init', function() {
var ngModelCtrl, config;
beforeEach(function() {
ngModelCtrl = {};
config = {
foo: 'bar',
itemsPerPage: 12
};
});
describe('without itemsPerPage', function() {
beforeEach(function() {
ctrl.init(ngModelCtrl, config);
});
it('should set the ngModel and config', function() {
expect(ctrl.ngModelCtrl).toBe(ngModelCtrl);
expect(ctrl.config).toBe(config);
});
it('should properly render the model', function() {
spyOn(ctrl, 'render');
ngModelCtrl.$render();
expect(ctrl.render).toHaveBeenCalled();
});
it('should set to default itemsPerPage', function() {
expect(ctrl.itemsPerPage).toBe(12);
});
it('should update the page when total items changes', function() {
spyOn(ctrl, 'calculateTotalPages').and.returnValue(5);
spyOn(ctrl, 'updatePage');
$rootScope.$digest();
expect(ctrl.calculateTotalPages.calls.count()).toBe(0);
expect(ctrl.updatePage.calls.count()).toBe(0);
$scope.totalItems = 10;
$rootScope.$digest();
expect(ctrl.calculateTotalPages.calls.count()).toBe(1);
expect(ctrl.updatePage.calls.count()).toBe(1);
expect($scope.totalPages).toBe(5);
$scope.totalItems = undefined;
$scope.totalPages = 2;
$rootScope.$digest();
expect(ctrl.calculateTotalPages.calls.count()).toBe(2);
expect(ctrl.updatePage.calls.count()).toBe(2);
expect($scope.totalPages).toBe(5);
});
});
describe('with itemsPerPage', function() {
beforeEach(function() {
attrs.itemsPerPage = 'abc';
$rootScope.abc = 10;
ctrl.init(ngModelCtrl, config);
});
it('should update the page when itemsPerPage changes', function() {
spyOn(ctrl, 'calculateTotalPages').and.returnValue(5);
spyOn(ctrl, 'updatePage');
$rootScope.$digest();
expect(ctrl.itemsPerPage).toBe(10);
expect($scope.totalPages).toBe(5);
expect(ctrl.updatePage).toHaveBeenCalled();
});
});
});
describe('calculate totalPages', function() {
it('when itemsPerPage is less than 1', function() {
ctrl.itemsPerPage = 0;
$scope.totalItems = 101;
expect(ctrl.calculateTotalPages()).toBe(1);
});
it('when itemsPerPage is greater than 1', function() {
ctrl.itemsPerPage = 10;
$scope.totalItems = 101;
expect(ctrl.calculateTotalPages()).toBe(11);
});
});
describe('render', function() {
it('should set page to 1 when invalid', function() {
ctrl.ngModelCtrl.$viewValue = 'abcd';
$scope.page = 10;
ctrl.render();
expect($scope.page).toBe(1);
});
it('should set page to view value when valid', function() {
ctrl.ngModelCtrl.$viewValue = '3';
$scope.page = 10;
ctrl.render();
expect($scope.page).toBe(3);
});
});
describe('select page', function() {
beforeEach(function() {
spyOn(ctrl.ngModelCtrl, '$setViewValue');
ctrl.ngModelCtrl.$render = jasmine.createSpy('ctrl.ngModelCtrl.$render');
$scope.page = 5;
$scope.totalPages = 20;
});
it('should change the page', function() {
$scope.selectPage(12);
expect(ctrl.ngModelCtrl.$setViewValue).toHaveBeenCalledWith(12);
expect(ctrl.ngModelCtrl.$render).toHaveBeenCalled();
});
it('should not change the page to one out of range', function() {
$scope.selectPage(-1);
expect(ctrl.ngModelCtrl.$setViewValue).not.toHaveBeenCalled();
expect(ctrl.ngModelCtrl.$render).not.toHaveBeenCalled();
$scope.selectPage(21);
expect(ctrl.ngModelCtrl.$setViewValue).not.toHaveBeenCalled();
expect(ctrl.ngModelCtrl.$render).not.toHaveBeenCalled();
});
describe('on click', function() {
var evt;
beforeEach(function() {
evt = {
preventDefault: jasmine.createSpy('evt.preventDefault'),
target: {
blur: jasmine.createSpy('evt.target.blur')
}
};
});
it('should prevent default behavior', function() {
$scope.selectPage(12, evt);
expect(evt.preventDefault).toHaveBeenCalled();
});
it('should not change the page if disabled and from an event', function() {
$scope.ngDisabled = true;
$scope.selectPage(12, evt);
expect(ctrl.ngModelCtrl.$setViewValue).not.toHaveBeenCalled();
expect(ctrl.ngModelCtrl.$render).not.toHaveBeenCalled();
});
it('should blur the element clicked', function() {
$scope.selectPage(12, evt);
expect(evt.target.blur).toHaveBeenCalled();
});
});
});
it('should get the text', function() {
$scope.fooText = 'bar';
expect($scope.getText('foo')).toBe('bar');
});
it('should get the default text', function() {
ctrl.config = {
fooText: 'bar'
};
expect($scope.getText('foo')).toBe('bar');
});
it('should disable previous button', function() {
$scope.page = 1;
expect($scope.noPrevious()).toBe(true);
});
it('should enable previous button', function() {
$scope.page = 2;
expect($scope.noPrevious()).toBe(false);
});
it('should disable next button', function() {
$scope.page = 10;
$scope.totalPages = 10;
expect($scope.noNext()).toBe(true);
});
it('should enable next button', function() {
$scope.page = 9;
$scope.totalPages = 10;
expect($scope.noNext()).toBe(false);
});
describe('update page', function() {
beforeEach(function() {
spyOn($scope, 'selectPage');
ctrl.ngModelCtrl.$render = jasmine.createSpy('ctrl.ngModelCtrl.$render');
ctrl.setNumPages = jasmine.createSpy('ctrl.setNumPages');
$scope.totalPages = 10;
});
it('should select the last page if page is above total', function() {
$scope.page = 12;
ctrl.updatePage();
expect(ctrl.setNumPages).toHaveBeenCalledWith($rootScope, 10);
expect($scope.selectPage).toHaveBeenCalledWith(10);
expect(ctrl.ngModelCtrl.$render).not.toHaveBeenCalled();
});
it('should execute render if page is within range', function() {
$scope.page = 5;
ctrl.updatePage();
expect(ctrl.setNumPages).toHaveBeenCalledWith($rootScope, 10);
expect($scope.selectPage).not.toHaveBeenCalled();
expect(ctrl.ngModelCtrl.$render).toHaveBeenCalled();
});
});
describe('gc', function() {
it('should clear watchers', function() {
var watcher1 = jasmine.createSpy('watcher1'),
watcher2 = jasmine.createSpy('watcher2');
ctrl._watchers = [watcher1, watcher2];
$scope.$destroy();
expect(ctrl._watchers.length).toBe(0);
expect(watcher1).toHaveBeenCalled();
expect(watcher2).toHaveBeenCalled();
});
});
});
================================================
FILE: src/popover/docs/demo.html
================================================
================================================
FILE: src/popover/docs/demo.js
================================================
angular.module('ui.bootstrap.demo').controller('PopoverDemoCtrl', function ($scope, $sce) {
$scope.dynamicPopover = {
content: 'Hello, World!',
templateUrl: 'myPopoverTemplate.html',
title: 'Title'
};
$scope.placement = {
options: [
'top',
'top-left',
'top-right',
'bottom',
'bottom-left',
'bottom-right',
'left',
'left-top',
'left-bottom',
'right',
'right-top',
'right-bottom'
],
selected: 'top'
};
$scope.htmlPopover = $sce.trustAsHtml('
I can have
HTML
content');
});
================================================
FILE: src/popover/docs/readme.md
================================================
A lightweight, extensible directive for fancy popover creation. The popover
directive supports multiple placements, optional transition animation, and more.
Like the Bootstrap jQuery plugin, the popover **requires** the tooltip
module.
__Note to mobile developers__: Please note that while popovers may work correctly on mobile devices (including tablets),
we have made the decision to not officially support such a use-case because it does not make sense from a UX perspective.
There are three versions of the popover: `uib-popover` and `uib-popover-template`, and `uib-popover-html`:
* `uib-popover` -
Takes text only and will escape any HTML provided for the popover body.
* `uib-popover-html`
$ -
Takes an expression that evaluates to an HTML string. Note that this HTML is not compiled. If compilation is required, please use the `uib-popover-template` attribute option instead. *The user is responsible for ensuring the content is safe to put into the DOM!*
* `uib-popover-template`
$ -
A URL representing the location of a template to use for the popover body. Note that the contents of this template need to be wrapped in a tag, e.g., `
`.
### uib-popover-* settings
All these settings are available for the three types of popovers.
* `popover-animation`
$
C
_(Default: `true`, Config: `animation`)_ -
Should it fade in and out?
* `popover-append-to-body`
$
C
_(Default: `false`, Config: `appendToBody`)_ -
Should the popover be appended to '$body' instead of the parent element?
* `popover-class` -
Custom class to be applied to the popover.
* `popover-enable`
$
_(Default: `true`)_ -
Is it enabled? It will enable or disable the configured popover-trigger.
* `popover-is-open`
_(Default: `false`)_ -
Whether to show the popover.
* `popover-placement`
C
_(Default: `top`, Config: `placement`)_ -
Passing in 'auto' separated by a space before the placement will enable auto positioning, e.g: "auto bottom-left". The popover will attempt to position where it fits in the closest scrollable ancestor. Accepts:
* `top` - popover on top, horizontally centered on host element.
* `top-left` - popover on top, left edge aligned with host element left edge.
* `top-right` - popover on top, right edge aligned with host element right edge.
* `bottom` - popover on bottom, horizontally centered on host element.
* `bottom-left` - popover on bottom, left edge aligned with host element left edge.
* `bottom-right` - popover on bottom, right edge aligned with host element right edge.
* `left` - popover on left, vertically centered on host element.
* `left-top` - popover on left, top edge aligned with host element top edge.
* `left-bottom` - popover on left, bottom edge aligned with host element bottom edge.
* `right` - popover on right, vertically centered on host element.
* `right-top` - popover on right, top edge aligned with host element top edge.
* `right-bottom` - popover on right, bottom edge aligned with host element bottom edge.
* `popover-popup-close-delay`
C
_(Default: `0`, Config: `popupCloseDelay`)_ -
For how long should the popover remain open after the close trigger event?
* `popover-popup-delay`
C
_(Default: `0`, Config: `popupDelay`)_ -
Popup delay in milliseconds until it opens.
* `popover-title` -
A string to display as a fancy title.
* `popover-trigger`
$
_(Default: `'click'`)_ -
What should trigger a show of the popover? Supports a space separated list of event names, or objects (see below).
**Note:** To configure the tooltips, you need to do it on `$uibTooltipProvider` (also see below).
### Triggers
The following show triggers are supported out of the box, along with their provided hide triggers:
- `mouseenter`: `mouseleave`
- `click`: `click`
- `outsideClick`: `outsideClick`
- `focus`: `blur`
- `none`
The `outsideClick` trigger will cause the popover to toggle on click, and hide when anything else is clicked.
For any non-supported value, the trigger will be used to both show and hide the
popover. Using the 'none' trigger will disable the internal trigger(s), one can
then use the `popover-is-open` attribute exclusively to show and hide the popover.
### $uibTooltipProvider
Through the `$uibTooltipProvider`, you can change the way tooltips and popovers
behave by default; the attributes above always take precedence. The following
methods are available:
* `setTriggers(obj)`
_(Example: `{ 'openTrigger': 'closeTrigger' }`)_ -
Extends the default trigger mappings mentioned above with mappings of your own.
* `options(obj)` -
Provide a set of defaults for certain tooltip and popover attributes. Currently supports the ones with the
C badge.
### Known issues
For Safari 7+ support, if you want to use **focus** `popover-trigger`, you need to use an anchor tag with a tab index. For example:
```
Click Me
```
================================================
FILE: src/popover/index-nocss.js
================================================
require('../tooltip/index-nocss.js');
require('../../template/popover/popover.html.js');
require('../../template/popover/popover-html.html.js');
require('../../template/popover/popover-template.html.js');
require('./popover');
var MODULE_NAME = 'ui.bootstrap.module.popover';
angular.module(MODULE_NAME, ['ui.bootstrap.popover', 'uib/template/popover/popover.html', 'uib/template/popover/popover-html.html', 'uib/template/popover/popover-template.html']);
module.exports = MODULE_NAME;
================================================
FILE: src/popover/index.js
================================================
require('../tooltip/tooltip.css');
module.exports = require('./index-nocss.js');
================================================
FILE: src/popover/popover.js
================================================
/**
* The following features are still outstanding: popup delay, animation as a
* function, placement as a function, inside, support for more triggers than
* just mouse enter/leave, and selector delegatation.
*/
angular.module('ui.bootstrap.popover', ['ui.bootstrap.tooltip'])
.directive('uibPopoverTemplatePopup', function() {
return {
restrict: 'A',
scope: { uibTitle: '@', contentExp: '&', originScope: '&' },
templateUrl: 'uib/template/popover/popover-template.html'
};
})
.directive('uibPopoverTemplate', ['$uibTooltip', function($uibTooltip) {
return $uibTooltip('uibPopoverTemplate', 'popover', 'click', {
useContentExp: true
});
}])
.directive('uibPopoverHtmlPopup', function() {
return {
restrict: 'A',
scope: { contentExp: '&', uibTitle: '@' },
templateUrl: 'uib/template/popover/popover-html.html'
};
})
.directive('uibPopoverHtml', ['$uibTooltip', function($uibTooltip) {
return $uibTooltip('uibPopoverHtml', 'popover', 'click', {
useContentExp: true
});
}])
.directive('uibPopoverPopup', function() {
return {
restrict: 'A',
scope: { uibTitle: '@', content: '@' },
templateUrl: 'uib/template/popover/popover.html'
};
})
.directive('uibPopover', ['$uibTooltip', function($uibTooltip) {
return $uibTooltip('uibPopover', 'popover', 'click');
}]);
================================================
FILE: src/popover/test/popover-html.spec.js
================================================
describe('popover', function() {
var elm,
elmBody,
scope,
elmScope,
tooltipScope;
// load the popover code
beforeEach(module('ui.bootstrap.popover'));
// load the template
beforeEach(module('uib/template/popover/popover-html.html'));
beforeEach(inject(function($rootScope, $compile, $sce, _$document_) {
$document = _$document_;
elmBody = angular.element(
'
Selector Text
'
);
scope = $rootScope;
scope.template = $sce.trustAsHtml('
My template');
$compile(elmBody)(scope);
scope.$digest();
$document.find('body').append(elmBody);
elm = elmBody.find('span');
elmScope = elm.scope();
tooltipScope = elmScope.$$childTail;
}));
afterEach(function() {
$document.off('keypress');
});
it('should not be open initially', inject(function() {
expect(tooltipScope.isOpen).toBe(false);
// We can only test *that* the popover-popup element wasn't created as the
// implementation is templated and replaced.
expect(elmBody.children().length).toBe(1);
}));
it('should open on click', inject(function() {
elm.trigger('click');
tooltipScope.$digest();
expect(tooltipScope.isOpen).toBe(true);
// We can only test *that* the popover-popup element was created as the
// implementation is templated and replaced.
expect(elmBody.children().length).toBe(2);
}));
it('should close on second click', inject(function() {
elm.trigger('click');
tooltipScope.$digest();
expect(tooltipScope.isOpen).toBe(true);
elm.trigger('click');
tooltipScope.$digest();
expect(tooltipScope.isOpen).toBe(false);
}));
it('should not open on click if template is empty', inject(function() {
scope.template = null;
scope.$digest();
elm.trigger('click');
tooltipScope.$digest();
expect(tooltipScope.isOpen).toBe(false);
expect(elmBody.children().length).toBe(1);
}));
it('should show updated text', inject(function($sce) {
scope.template = $sce.trustAsHtml('
My template');
scope.$digest();
elm.trigger('click');
tooltipScope.$digest();
expect(tooltipScope.isOpen).toBe(true);
expect(elmBody.children().eq(1).text().trim()).toBe('My template');
scope.template = $sce.trustAsHtml('
Another template');
scope.$digest();
expect(elmBody.children().eq(1).text().trim()).toBe('Another template');
}));
it('should hide popover when template becomes empty', inject(function($timeout) {
elm.trigger('click');
tooltipScope.$digest();
$timeout.flush(0);
expect(tooltipScope.isOpen).toBe(true);
scope.template = '';
scope.$digest();
expect(tooltipScope.isOpen).toBe(false);
$timeout.flush();
expect(elmBody.children().length).toBe(1);
}));
it('should not unbind event handlers created by other directives - issue 456', inject(function($compile) {
scope.click = function() {
scope.clicked = !scope.clicked;
};
elmBody = angular.element(
'
'
);
$compile(elmBody)(scope);
scope.$digest();
elm = elmBody.find('input');
elm.trigger('mouseenter');
tooltipScope.$digest();
elm.trigger('mouseleave');
tooltipScope.$digest();
expect(scope.clicked).toBeFalsy();
elm.click();
expect(scope.clicked).toBeTruthy();
}));
it('should popup with animate class by default', inject(function() {
elm.trigger('click');
tooltipScope.$digest();
expect(tooltipScope.isOpen).toBe(true);
expect(elmBody.children().eq(1)).toHaveClass('fade');
}));
it('should popup without animate class when animation disabled', inject(function($compile) {
elmBody = angular.element(
'
Selector Text
'
);
$compile(elmBody)(scope);
scope.$digest();
elm = elmBody.find('span');
elmScope = elm.scope();
tooltipScope = elmScope.$$childTail;
elm.trigger('click');
tooltipScope.$digest();
expect(tooltipScope.isOpen).toBe(true);
expect(elmBody.children().eq(1)).not.toHaveClass('fade');
}));
it ('should display the title', inject(function($compile) {
elmBody = angular.element(
'
Selector Text
'
);
$compile(elmBody)(scope);
scope.$digest();
elm = elmBody.find('span');
elm.trigger('click');
scope.$digest();
var titleEl = elmBody.find('.popover-title');
expect(titleEl.text()).toBe('popover title');
}));
describe('supports options', function() {
describe('placement', function() {
it('can specify an alternative, valid placement', inject(function($compile) {
elmBody = angular.element(
'
Trigger here
'
);
$compile(elmBody)(scope);
scope.$digest();
elm = elmBody.find('span');
elmScope = elm.scope();
tooltipScope = elmScope.$$childTail;
elm.trigger('click');
tooltipScope.$digest();
expect(tooltipScope.isOpen).toBe(true);
expect(elmBody.children().length).toBe(2);
var ttipElement = elmBody.find('div.popover');
expect(ttipElement).toHaveClass('left');
}));
});
describe('class', function() {
it('can specify a custom class', inject(function($compile) {
elmBody = angular.element(
'
Trigger here
'
);
$compile(elmBody)(scope);
scope.$digest();
elm = elmBody.find('span');
elmScope = elm.scope();
tooltipScope = elmScope.$$childTail;
elm.trigger('click');
tooltipScope.$digest();
expect(tooltipScope.isOpen).toBe(true);
expect(elmBody.children().length).toBe(2);
var ttipElement = elmBody.find('div.popover');
expect(ttipElement).toHaveClass('custom');
}));
});
});
});
================================================
FILE: src/popover/test/popover-template.spec.js
================================================
describe('popover template', function() {
var elm,
elmBody,
scope,
elmScope,
tooltipScope,
$document;
// load the popover code
beforeEach(module('ui.bootstrap.popover'));
// load the template
beforeEach(module('uib/template/popover/popover.html'));
beforeEach(module('uib/template/popover/popover-template.html'));
beforeEach(inject(function($templateCache) {
$templateCache.put('myUrl', [200, '
{{ myTemplateText }}', {}]);
}));
beforeEach(inject(function($rootScope, $compile, _$document_) {
$document = _$document_;
elmBody = angular.element(
'
Selector Text
'
);
scope = $rootScope;
$compile(elmBody)(scope);
$document.find('body').append(elmBody);
scope.templateUrl = 'myUrl';
scope.$digest();
elm = elmBody.find('span');
elmScope = elm.scope();
tooltipScope = elmScope.$$childTail;
}));
afterEach(function() {
$document.off('keypress');
elmBody.remove();
});
it('should open on click', inject(function() {
elm.trigger('click');
tooltipScope.$digest();
expect(tooltipScope.isOpen).toBe(true);
expect(elmBody.children().length ).toBe(2);
}));
it('should not open on click if templateUrl is empty', inject(function() {
scope.templateUrl = null;
scope.$digest();
elm.trigger('click');
tooltipScope.$digest();
expect(tooltipScope.isOpen).toBe(false);
expect(elmBody.children().length).toBe(1);
}));
it('should show updated text', inject(function() {
scope.myTemplateText = 'some text';
elm.trigger('click');
tooltipScope.$digest();
expect(tooltipScope.isOpen).toBe(true);
scope.$digest();
expect(elmBody.children().eq(1).text().trim()).toBe('some text');
scope.myTemplateText = 'new text';
scope.$digest();
expect(elmBody.children().eq(1).text().trim()).toBe('new text');
}));
it('should hide popover when template becomes empty', inject(function($timeout) {
elm.trigger('click');
tooltipScope.$digest();
$timeout.flush(0);
expect(tooltipScope.isOpen).toBe(true);
scope.templateUrl = '';
scope.$digest();
expect(tooltipScope.isOpen).toBe(false);
$timeout.flush();
expect(elmBody.children().length).toBe(1);
}));
it ('should display the title', inject(function($compile) {
elmBody = angular.element(
'
Selector Text
'
);
$compile(elmBody)(scope);
scope.$digest();
elm = elmBody.find('span');
elm.trigger('click');
scope.$digest();
var titleEl = elmBody.find('.popover-title');
expect(titleEl.text()).toBe('popover title');
}));
describe('supports options', function() {
describe('placement', function() {
it('can specify an alternative, valid placement', inject(function($compile) {
elmBody = angular.element(
'
Trigger
'
);
$compile(elmBody)(scope);
scope.$digest();
elm = elmBody.find('span');
elmScope = elm.scope();
tooltipScope = elmScope.$$childTail;
elm.trigger('click');
tooltipScope.$digest();
expect(tooltipScope.isOpen).toBe(true);
expect(elmBody.children().length).toBe(2);
var ttipElement = elmBody.find('div.popover');
expect(ttipElement).toHaveClass('left');
}));
});
describe('class', function() {
it('can specify a custom class', inject(function($compile) {
elmBody = angular.element(
'
Trigger
'
);
$compile(elmBody)(scope);
scope.$digest();
elm = elmBody.find('span');
elmScope = elm.scope();
tooltipScope = elmScope.$$childTail;
elm.trigger('click');
tooltipScope.$digest();
expect(tooltipScope.isOpen).toBe(true);
expect(elmBody.children().length).toBe(2);
var ttipElement = elmBody.find('div.popover');
expect(ttipElement).toHaveClass('custom');
}));
});
});
});
================================================
FILE: src/popover/test/popover.spec.js
================================================
describe('popover', function() {
var elm,
elmBody,
scope,
elmScope,
tooltipScope,
$document;
// load the popover code
beforeEach(module('ui.bootstrap.popover'));
// load the template
beforeEach(module('uib/template/popover/popover.html'));
beforeEach(inject(function($rootScope, $compile, _$document_) {
$document = _$document_;
elmBody = angular.element(
'
Selector Text
'
);
scope = $rootScope;
$compile(elmBody)(scope);
scope.$digest();
elm = elmBody.find('span');
elmScope = elm.scope();
tooltipScope = elmScope.$$childTail;
}));
afterEach(function() {
$document.off('keypress');
});
it('should not be open initially', inject(function() {
expect(tooltipScope.isOpen).toBe(false);
// We can only test *that* the popover-popup element wasn't created as the
// implementation is templated and replaced.
expect(elmBody.children().length).toBe(1);
}));
it('should open on click', inject(function() {
elm.trigger('click');
tooltipScope.$digest();
expect(tooltipScope.isOpen).toBe(true);
// We can only test *that* the popover-popup element was created as the
// implementation is templated and replaced.
expect(elmBody.children().length).toBe(2);
}));
it('should close on second click', inject(function() {
elm.trigger('click');
tooltipScope.$digest();
expect(tooltipScope.isOpen).toBe(true);
elm.trigger('click');
tooltipScope.$digest();
expect(tooltipScope.isOpen).toBe(false);
}));
it('should not unbind event handlers created by other directives - issue 456', inject(function($compile) {
scope.click = function() {
scope.clicked = !scope.clicked;
};
elmBody = angular.element(
'
'
);
$compile(elmBody)(scope);
scope.$digest();
elm = elmBody.find('input');
elm.trigger('mouseenter');
elm.trigger('mouseleave');
expect(scope.clicked).toBeFalsy();
elm.click();
expect(scope.clicked).toBeTruthy();
}));
it('should popup with animate class by default', inject(function() {
elm.trigger('click');
tooltipScope.$digest();
expect(tooltipScope.isOpen).toBe(true);
expect(elmBody.children().eq(1)).toHaveClass('fade');
}));
it('should popup without animate class when animation disabled', inject(function($compile) {
elmBody = angular.element(
'
Selector Text
'
);
$compile(elmBody)(scope);
scope.$digest();
elm = elmBody.find('span');
elmScope = elm.scope();
tooltipScope = elmScope.$$childTail;
elm.trigger('click');
tooltipScope.$digest();
expect(tooltipScope.isOpen).toBe(true);
expect(elmBody.children().eq(1)).not.toHaveClass('fade');
}));
it ('should display the title', inject(function($compile) {
elmBody = angular.element(
'
Trigger here
'
);
$compile(elmBody)(scope);
scope.$digest();
elm = elmBody.find('span');
elm.trigger('click');
scope.$digest();
var titleEl = elmBody.find('.popover-title');
expect(titleEl.text()).toBe('popover title');
}));
it ('should display the content', inject(function($compile) {
elmBody = angular.element(
'
Trigger here
'
);
$compile(elmBody)(scope);
scope.$digest();
elm = elmBody.find('span');
elm.trigger('click');
scope.$digest();
var contentEl = elmBody.find('.popover-content');
expect(contentEl.text()).toBe('popover text');
}));
describe('supports options', function() {
describe('placement', function() {
it('can specify an alternative, valid placement', inject(function($compile) {
elmBody = angular.element(
'
Trigger here
'
);
$compile(elmBody)(scope);
scope.$digest();
elm = elmBody.find('span');
elmScope = elm.scope();
tooltipScope = elmScope.$$childTail;
elm.trigger('click');
tooltipScope.$digest();
expect(tooltipScope.isOpen).toBe(true);
expect(elmBody.children().length).toBe(2);
var ttipElement = elmBody.find('div.popover');
expect(ttipElement).toHaveClass('left');
}));
});
describe('class', function() {
it('can specify a custom class', inject(function($compile) {
elmBody = angular.element(
'
Trigger here
'
);
$compile(elmBody)(scope);
scope.$digest();
elm = elmBody.find('span');
elmScope = elm.scope();
tooltipScope = elmScope.$$childTail;
elm.trigger('click');
tooltipScope.$digest();
expect(tooltipScope.isOpen).toBe(true);
expect(elmBody.children().length).toBe(2);
var ttipElement = elmBody.find('div.popover');
expect(ttipElement).toHaveClass('custom');
}));
});
describe('is-open', function() {
beforeEach(inject(function ($compile) {
scope.isOpen = false;
elmBody = angular.element(
'
Trigger here
'
);
$compile(elmBody)(scope);
scope.$digest();
elm = elmBody.find('span');
elmScope = elm.scope();
tooltipScope = elmScope.$$childTail;
}));
it('should show and hide with the controller value', function() {
expect(tooltipScope.isOpen).toBe(false);
elmScope.isOpen = true;
elmScope.$digest();
expect(tooltipScope.isOpen).toBe(true);
elmScope.isOpen = false;
elmScope.$digest();
expect(tooltipScope.isOpen).toBe(false);
});
it('should update the controller value', function() {
elm.trigger('click');
tooltipScope.$digest();
expect(elmScope.isOpen).toBe(true);
elm.trigger('click');
tooltipScope.$digest();
expect(elmScope.isOpen).toBe(false);
});
});
});
});
================================================
FILE: src/position/docs/demo.html
================================================
$uibPosition service
offsetParent: {{elemVals.offsetParent}}
scrollParent: {{elemVals.scrollParent}}
scrollbarWidth: {{scrollbarWidth}}
position: {{elemVals.position}}
offset: {{elemVals.offset}}
viewportOffset: {{elemVals.viewportOffset}}
positionElements: {{elemVals.positionElements}}
================================================
FILE: src/position/docs/demo.js
================================================
angular.module('ui.bootstrap.demo').controller('PositionDemoCtrl', function ($scope, $window, $uibPosition) {
$scope.elemVals = {};
$scope.parentScrollable = true;
$scope.parentRelative = true;
$scope.getValues = function() {
var divEl = $window.document.querySelector('#posdemodiv');
var btnEl = $window.document.querySelector('#posdemobtn');
var offsetParent = $uibPosition.offsetParent(divEl);
$scope.elemVals.offsetParent = 'type: ' + offsetParent.tagName + ', id: ' + offsetParent.id;
var scrollParent = $uibPosition.scrollParent(divEl);
$scope.elemVals.scrollParent = 'type: ' + scrollParent.tagName + ', id: ' + scrollParent.id;
$scope.scrollbarWidth = $uibPosition.scrollbarWidth();
$scope.elemVals.position = $uibPosition.position(divEl);
$scope.elemVals.offset = $uibPosition.offset(divEl);
$scope.elemVals.viewportOffset = $uibPosition.viewportOffset(divEl);
$scope.elemVals.positionElements = $uibPosition.positionElements(btnEl, divEl, 'auto bottom-left');
};
});
================================================
FILE: src/position/docs/readme.md
================================================
The `$uibPosition` service provides a set of DOM utilities used internally to absolute-position an element in relation to another element (tooltips, popovers, typeaheads etc...).
#### getRawNode(element)
Takes a jQuery/jqLite element and converts it to a raw DOM element.
##### parameters
* `element`
_(Type: `object`)_ -
The element to convert.
##### returns
* _(Type: `element`)_ -
A raw DOM element.
#### parseStyle(element)
Parses a numeric style value to a number. Strips units and will return 0 for invalid (NaN) numbers.
##### parameters
* `value`
_(Type: `string`)_ -
The style value to parse.
##### returns
* _(Type: `number`)_ -
The numeric value of the style property.
#### offsetParent(element)
Gets the closest positioned ancestor.
##### parameters
* `element`
_(Type: `element`)_ -
The element to get the offset parent for.
##### returns
* _(Type: `element`)_ -
The closest positioned ancestor.
#### scrollbarWidth(isBody)
Calculates the browser scrollbar width and caches the result for future calls. Concept from the TWBS measureScrollbar() function in [modal.js](https://github.com/twbs/bootstrap/blob/master/js/modal.js).
##### parameters
* `isBody`
_(Type: `boolean`, Default: `false`, optional)_ - Is the requested scrollbar width for the body/html element. IE and Edge overlay the scrollbar on the body/html element and should be considered 0.
##### returns
* _(Type: `number`)_ -
The width of the browser scrollbar.
#### scrollbarPadding(element)
Calculates the padding required to replace the scrollbar on an element.
##### parameters
* 'element' _(Type: `element`)_ - The element to calculate the padding on (should be a scrollable element).
##### returns
An object with the following properties:
* `scrollbarWidth`
_(Type: `number`)_ -
The width of the scrollbar.
* `widthOverflow`
_(Type: `boolean`)_ -
Whether the width is overflowing.
* `right`
_(Type: `number`)_ -
The total right padding required to replace the scrollbar.
* `originalRight`
_(Type: `number`)_ -
The oringal right padding on the element.
* `heightOverflow`
_(Type: `boolean`)_ -
Whether the height is overflowing.
* `bottom`
_(Type: `number`)_ -
The total bottom padding required to replace the scrollbar.
* `originalBottom`
_(Type: `number`)_ -
The oringal bottom padding on the element.
#### isScrollable(element, includeHidden)
Determines if an element is scrollable.
##### parameters
* `element`
_(Type: `element`)_ -
The element to check.
* `includeHidden`
_(Type: `boolean`, Default: `false`, optional)_ - Should scroll style of 'hidden' be considered.
##### returns
* _(Type: `boolean`)_ -
Whether the element is scrollable.
#### scrollParent(element, includeHidden, includeSelf)
Gets the closest scrollable ancestor. Concept from the jQueryUI [scrollParent.js](https://github.com/jquery/jquery-ui/blob/master/ui/scroll-parent.js).
##### parameters
* `element`
_(Type: `element`)_ -
The element to get the closest scrollable ancestor for.
* `includeHidden`
_(Type: `boolean`, Default: `false`, optional)_ - Should scroll style of 'hidden' be considered.
* `includeSelf`
_(Type: `boolean`, Default: `false`, optional)_ - Should the element passed in be included in the scrollable lookup.
##### returns
* _(Type: `element`)_ -
The closest scrollable ancestor.
#### position(element, includeMargins)
A read-only equivalent of jQuery's [position](http://api.jquery.com/position/) function, distance to closest positioned ancestor. Does not account for margins by default like jQuery's position.
##### parameters
* `element` _(Type: `element`)_ -
The element to get the position for.
* `includeMargins` _(Type: `boolean`, Default: `false`, optional)_ -
Should margins be accounted for.
##### returns
An object with the following properties:
* `width`
_(Type: `number`)_ -
The width of the element.
* `height`
_(Type: `number`)_ -
The height of the element.
* `top`
_(Type: `number`)_ -
Distance to top edge of offset parent.
* `left`
_(Type: `number`)_ -
Distance to left edge of offset parent.
#### offset(element)
A read-only equivalent of jQuery's [offset](http://api.jquery.com/offset/) function, distance to viewport.
##### parameters
* `element`
_(Type: `element`)_ -
The element to get the offset for.
##### returns
An object with the following properties:
* `width`
_(Type: `number`)_ -
The width of the element.
* `height`
_(Type: `number`)_ -
The height of the element.
* `top`
_(Type: `number`)_ -
Distance to top edge of the viewport.
* `left`
_(Type: `number`)_ -
Distance to left edge of the viewport.
#### viewportOffset(element, useDocument, includePadding)
Gets the elements available space relative to the closest scrollable ancestor. Accounts for padding, border, and scrollbar width.
Right and bottom dimensions represent the distance to the respective edge of the viewport element, not the top and left edge.
If the element edge extends beyond the viewport, a negative value will be reported.
##### parameters
* `element`
_(Type: `element`)_ -
The element to get the viewport offset for.
* `useDocument`
_(Type: `boolean`, Default: `false`, optional)_ -
Should the viewport be the document element instead of the first scrollable element.
* `includePadding`
_(Type: `boolean`, Default: `true`, optional)_ -
Should the padding on the viewport element be accounted for, default is true.
##### returns
An object with the following properties:
* `top`
_(Type: `number`)_ -
Distance to top content edge of the viewport.
* `bottom`
_(Type: `number`)_ -
Distance to bottom content edge of the viewport.
* `left`
_(Type: `number`)_ -
Distance to left content edge of the viewport.
* `right`
_(Type: `number`)_ -
Distance to right content edge of the viewport.
#### parsePlacement(placement)
Gets an array of placement values parsed from a placement string. Along with the 'auto' indicator, supported placement strings are:
* top: element on top, horizontally centered on host element.
* top-left: element on top, left edge aligned with host element left edge.
* top-right: element on top, right edge aligned with host element right edge.
* bottom: element on bottom, horizontally centered on host element.
* bottom-left: element on bottom, left edge aligned with host element left edge.
* bottom-right: element on bottom, right edge aligned with host element right edge.
* left: element on left, vertically centered on host element.
* left-top: element on left, top edge aligned with host element top edge.
* left-bottom: element on left, bottom edge aligned with host element bottom edge.
* right: element on right, vertically centered on host element.
* right-top: element on right, top edge aligned with host element top edge.
* right-bottom: element on right, bottom edge aligned with host element bottom edge.
A placement string with an 'auto' indicator is expected to be space separated from the placement, i.e: 'auto bottom-left'.
If the primary and secondary placement values do not match 'top, bottom, left, right' then 'top' will be the primary placement and
'center' will be the secondary placement. If 'auto' is passed, true will be returned as the 3rd value of the array.
##### parameters
* `placement`
_(Type: `string`, Example: `auto top-left`)_ -
The placement string to parse.
##### returns
An array with the following values:
* `[0]`
_(Type: `string`)_ -
The primary placement.
* `[1]`
_(Type: `string`)_ -
The secondary placement.
* `[2]`
_(Type: `boolean`)_ -
Is auto place enabled.
#### positionElements(hostElement, targetElement, placement, appendToBody)
Gets gets coordinates for an element to be positioned relative to another element.
##### parameters
* `hostElement`
_(Type: `element`)_ -
The element to position against.
* `targetElement`
_(Type: `element`)_ -
The element to position.
* `placement`
_(Type: `string`, Default: `top`, optional)_ -
The placement for the target element. See the parsePlacement() function for available options. If 'auto' placement is used, the viewportOffset() function is used to decide where the targetElement will fit.
* `appendToBody`
_(Type: `boolean`, Default: `false`, optional)_ -
Should the coordinates be cacluated from the body element.
##### returns
An object with the following properties:
* `top`
_(Type: `number`)_ -
The targetElement top value.
* `left`
_(Type: `number`)_ -
The targetElement left value.
* `right`
_(Type: `number`)_ -
The resolved placement with 'auto' removed.
#### positionArrow(element, placement)
Positions the tooltip and popover arrow elements when using placement options beyond the standard top, left, bottom, or right.
##### parameters
* `element`
_(Type: `element`)_ -
The element to position the arrow element for.
* `placement`
_(Type: `string`)_ -
The placement for the element.
================================================
FILE: src/position/index-nocss.js
================================================
require('./position');
var MODULE_NAME = 'ui.bootstrap.module.position';
angular.module(MODULE_NAME, ['ui.bootstrap.position']);
module.exports = MODULE_NAME;
================================================
FILE: src/position/index.js
================================================
require('./position.css');
module.exports = require('./index-nocss.js');
================================================
FILE: src/position/position.css
================================================
.uib-position-measure {
display: block !important;
visibility: hidden !important;
position: absolute !important;
top: -9999px !important;
left: -9999px !important;
}
.uib-position-scrollbar-measure {
position: absolute !important;
top: -9999px !important;
width: 50px !important;
height: 50px !important;
overflow: scroll !important;
}
.uib-position-body-scrollbar-measure {
overflow: scroll !important;
}
================================================
FILE: src/position/position.js
================================================
angular.module('ui.bootstrap.position', [])
/**
* A set of utility methods for working with the DOM.
* It is meant to be used where we need to absolute-position elements in
* relation to another element (this is the case for tooltips, popovers,
* typeahead suggestions etc.).
*/
.factory('$uibPosition', ['$document', '$window', function($document, $window) {
/**
* Used by scrollbarWidth() function to cache scrollbar's width.
* Do not access this variable directly, use scrollbarWidth() instead.
*/
var SCROLLBAR_WIDTH;
/**
* scrollbar on body and html element in IE and Edge overlay
* content and should be considered 0 width.
*/
var BODY_SCROLLBAR_WIDTH;
var OVERFLOW_REGEX = {
normal: /(auto|scroll)/,
hidden: /(auto|scroll|hidden)/
};
var PLACEMENT_REGEX = {
auto: /\s?auto?\s?/i,
primary: /^(top|bottom|left|right)$/,
secondary: /^(top|bottom|left|right|center)$/,
vertical: /^(top|bottom)$/
};
var BODY_REGEX = /(HTML|BODY)/;
return {
/**
* Provides a raw DOM element from a jQuery/jQLite element.
*
* @param {element} elem - The element to convert.
*
* @returns {element} A HTML element.
*/
getRawNode: function(elem) {
return elem.nodeName ? elem : elem[0] || elem;
},
/**
* Provides a parsed number for a style property. Strips
* units and casts invalid numbers to 0.
*
* @param {string} value - The style value to parse.
*
* @returns {number} A valid number.
*/
parseStyle: function(value) {
value = parseFloat(value);
return isFinite(value) ? value : 0;
},
/**
* Provides the closest positioned ancestor.
*
* @param {element} element - The element to get the offest parent for.
*
* @returns {element} The closest positioned ancestor.
*/
offsetParent: function(elem) {
elem = this.getRawNode(elem);
var offsetParent = elem.offsetParent || $document[0].documentElement;
function isStaticPositioned(el) {
return ($window.getComputedStyle(el).position || 'static') === 'static';
}
while (offsetParent && offsetParent !== $document[0].documentElement && isStaticPositioned(offsetParent)) {
offsetParent = offsetParent.offsetParent;
}
return offsetParent || $document[0].documentElement;
},
/**
* Provides the scrollbar width, concept from TWBS measureScrollbar()
* function in https://github.com/twbs/bootstrap/blob/master/js/modal.js
* In IE and Edge, scollbar on body and html element overlay and should
* return a width of 0.
*
* @returns {number} The width of the browser scollbar.
*/
scrollbarWidth: function(isBody) {
if (isBody) {
if (angular.isUndefined(BODY_SCROLLBAR_WIDTH)) {
var bodyElem = $document.find('body');
bodyElem.addClass('uib-position-body-scrollbar-measure');
BODY_SCROLLBAR_WIDTH = $window.innerWidth - bodyElem[0].clientWidth;
BODY_SCROLLBAR_WIDTH = isFinite(BODY_SCROLLBAR_WIDTH) ? BODY_SCROLLBAR_WIDTH : 0;
bodyElem.removeClass('uib-position-body-scrollbar-measure');
}
return BODY_SCROLLBAR_WIDTH;
}
if (angular.isUndefined(SCROLLBAR_WIDTH)) {
var scrollElem = angular.element('
');
$document.find('body').append(scrollElem);
SCROLLBAR_WIDTH = scrollElem[0].offsetWidth - scrollElem[0].clientWidth;
SCROLLBAR_WIDTH = isFinite(SCROLLBAR_WIDTH) ? SCROLLBAR_WIDTH : 0;
scrollElem.remove();
}
return SCROLLBAR_WIDTH;
},
/**
* Provides the padding required on an element to replace the scrollbar.
*
* @returns {object} An object with the following properties:
*
* - **scrollbarWidth**: the width of the scrollbar
* - **widthOverflow**: whether the the width is overflowing
* - **right**: the amount of right padding on the element needed to replace the scrollbar
* - **rightOriginal**: the amount of right padding currently on the element
* - **heightOverflow**: whether the the height is overflowing
* - **bottom**: the amount of bottom padding on the element needed to replace the scrollbar
* - **bottomOriginal**: the amount of bottom padding currently on the element
*
*/
scrollbarPadding: function(elem) {
elem = this.getRawNode(elem);
var elemStyle = $window.getComputedStyle(elem);
var paddingRight = this.parseStyle(elemStyle.paddingRight);
var paddingBottom = this.parseStyle(elemStyle.paddingBottom);
var scrollParent = this.scrollParent(elem, false, true);
var scrollbarWidth = this.scrollbarWidth(BODY_REGEX.test(scrollParent.tagName));
return {
scrollbarWidth: scrollbarWidth,
widthOverflow: scrollParent.scrollWidth > scrollParent.clientWidth,
right: paddingRight + scrollbarWidth,
originalRight: paddingRight,
heightOverflow: scrollParent.scrollHeight > scrollParent.clientHeight,
bottom: paddingBottom + scrollbarWidth,
originalBottom: paddingBottom
};
},
/**
* Checks to see if the element is scrollable.
*
* @param {element} elem - The element to check.
* @param {boolean=} [includeHidden=false] - Should scroll style of 'hidden' be considered,
* default is false.
*
* @returns {boolean} Whether the element is scrollable.
*/
isScrollable: function(elem, includeHidden) {
elem = this.getRawNode(elem);
var overflowRegex = includeHidden ? OVERFLOW_REGEX.hidden : OVERFLOW_REGEX.normal;
var elemStyle = $window.getComputedStyle(elem);
return overflowRegex.test(elemStyle.overflow + elemStyle.overflowY + elemStyle.overflowX);
},
/**
* Provides the closest scrollable ancestor.
* A port of the jQuery UI scrollParent method:
* https://github.com/jquery/jquery-ui/blob/master/ui/scroll-parent.js
*
* @param {element} elem - The element to find the scroll parent of.
* @param {boolean=} [includeHidden=false] - Should scroll style of 'hidden' be considered,
* default is false.
* @param {boolean=} [includeSelf=false] - Should the element being passed be
* included in the scrollable llokup.
*
* @returns {element} A HTML element.
*/
scrollParent: function(elem, includeHidden, includeSelf) {
elem = this.getRawNode(elem);
var overflowRegex = includeHidden ? OVERFLOW_REGEX.hidden : OVERFLOW_REGEX.normal;
var documentEl = $document[0].documentElement;
var elemStyle = $window.getComputedStyle(elem);
if (includeSelf && overflowRegex.test(elemStyle.overflow + elemStyle.overflowY + elemStyle.overflowX)) {
return elem;
}
var excludeStatic = elemStyle.position === 'absolute';
var scrollParent = elem.parentElement || documentEl;
if (scrollParent === documentEl || elemStyle.position === 'fixed') {
return documentEl;
}
while (scrollParent.parentElement && scrollParent !== documentEl) {
var spStyle = $window.getComputedStyle(scrollParent);
if (excludeStatic && spStyle.position !== 'static') {
excludeStatic = false;
}
if (!excludeStatic && overflowRegex.test(spStyle.overflow + spStyle.overflowY + spStyle.overflowX)) {
break;
}
scrollParent = scrollParent.parentElement;
}
return scrollParent;
},
/**
* Provides read-only equivalent of jQuery's position function:
* http://api.jquery.com/position/ - distance to closest positioned
* ancestor. Does not account for margins by default like jQuery position.
*
* @param {element} elem - The element to caclulate the position on.
* @param {boolean=} [includeMargins=false] - Should margins be accounted
* for, default is false.
*
* @returns {object} An object with the following properties:
*
* - **width**: the width of the element
* - **height**: the height of the element
* - **top**: distance to top edge of offset parent
* - **left**: distance to left edge of offset parent
*
*/
position: function(elem, includeMagins) {
elem = this.getRawNode(elem);
var elemOffset = this.offset(elem);
if (includeMagins) {
var elemStyle = $window.getComputedStyle(elem);
elemOffset.top -= this.parseStyle(elemStyle.marginTop);
elemOffset.left -= this.parseStyle(elemStyle.marginLeft);
}
var parent = this.offsetParent(elem);
var parentOffset = {top: 0, left: 0};
if (parent !== $document[0].documentElement) {
parentOffset = this.offset(parent);
parentOffset.top += parent.clientTop - parent.scrollTop;
parentOffset.left += parent.clientLeft - parent.scrollLeft;
}
return {
width: Math.round(angular.isNumber(elemOffset.width) ? elemOffset.width : elem.offsetWidth),
height: Math.round(angular.isNumber(elemOffset.height) ? elemOffset.height : elem.offsetHeight),
top: Math.round(elemOffset.top - parentOffset.top),
left: Math.round(elemOffset.left - parentOffset.left)
};
},
/**
* Provides read-only equivalent of jQuery's offset function:
* http://api.jquery.com/offset/ - distance to viewport. Does
* not account for borders, margins, or padding on the body
* element.
*
* @param {element} elem - The element to calculate the offset on.
*
* @returns {object} An object with the following properties:
*
* - **width**: the width of the element
* - **height**: the height of the element
* - **top**: distance to top edge of viewport
* - **right**: distance to bottom edge of viewport
*
*/
offset: function(elem) {
elem = this.getRawNode(elem);
var elemBCR = elem.getBoundingClientRect();
return {
width: Math.round(angular.isNumber(elemBCR.width) ? elemBCR.width : elem.offsetWidth),
height: Math.round(angular.isNumber(elemBCR.height) ? elemBCR.height : elem.offsetHeight),
top: Math.round(elemBCR.top + ($window.pageYOffset || $document[0].documentElement.scrollTop)),
left: Math.round(elemBCR.left + ($window.pageXOffset || $document[0].documentElement.scrollLeft))
};
},
/**
* Provides offset distance to the closest scrollable ancestor
* or viewport. Accounts for border and scrollbar width.
*
* Right and bottom dimensions represent the distance to the
* respective edge of the viewport element. If the element
* edge extends beyond the viewport, a negative value will be
* reported.
*
* @param {element} elem - The element to get the viewport offset for.
* @param {boolean=} [useDocument=false] - Should the viewport be the document element instead
* of the first scrollable element, default is false.
* @param {boolean=} [includePadding=true] - Should the padding on the offset parent element
* be accounted for, default is true.
*
* @returns {object} An object with the following properties:
*
* - **top**: distance to the top content edge of viewport element
* - **bottom**: distance to the bottom content edge of viewport element
* - **left**: distance to the left content edge of viewport element
* - **right**: distance to the right content edge of viewport element
*
*/
viewportOffset: function(elem, useDocument, includePadding) {
elem = this.getRawNode(elem);
includePadding = includePadding !== false ? true : false;
var elemBCR = elem.getBoundingClientRect();
var offsetBCR = {top: 0, left: 0, bottom: 0, right: 0};
var offsetParent = useDocument ? $document[0].documentElement : this.scrollParent(elem);
var offsetParentBCR = offsetParent.getBoundingClientRect();
offsetBCR.top = offsetParentBCR.top + offsetParent.clientTop;
offsetBCR.left = offsetParentBCR.left + offsetParent.clientLeft;
if (offsetParent === $document[0].documentElement) {
offsetBCR.top += $window.pageYOffset;
offsetBCR.left += $window.pageXOffset;
}
offsetBCR.bottom = offsetBCR.top + offsetParent.clientHeight;
offsetBCR.right = offsetBCR.left + offsetParent.clientWidth;
if (includePadding) {
var offsetParentStyle = $window.getComputedStyle(offsetParent);
offsetBCR.top += this.parseStyle(offsetParentStyle.paddingTop);
offsetBCR.bottom -= this.parseStyle(offsetParentStyle.paddingBottom);
offsetBCR.left += this.parseStyle(offsetParentStyle.paddingLeft);
offsetBCR.right -= this.parseStyle(offsetParentStyle.paddingRight);
}
return {
top: Math.round(elemBCR.top - offsetBCR.top),
bottom: Math.round(offsetBCR.bottom - elemBCR.bottom),
left: Math.round(elemBCR.left - offsetBCR.left),
right: Math.round(offsetBCR.right - elemBCR.right)
};
},
/**
* Provides an array of placement values parsed from a placement string.
* Along with the 'auto' indicator, supported placement strings are:
*
* - top: element on top, horizontally centered on host element.
* - top-left: element on top, left edge aligned with host element left edge.
* - top-right: element on top, lerightft edge aligned with host element right edge.
* - bottom: element on bottom, horizontally centered on host element.
* - bottom-left: element on bottom, left edge aligned with host element left edge.
* - bottom-right: element on bottom, right edge aligned with host element right edge.
* - left: element on left, vertically centered on host element.
* - left-top: element on left, top edge aligned with host element top edge.
* - left-bottom: element on left, bottom edge aligned with host element bottom edge.
* - right: element on right, vertically centered on host element.
* - right-top: element on right, top edge aligned with host element top edge.
* - right-bottom: element on right, bottom edge aligned with host element bottom edge.
*
* A placement string with an 'auto' indicator is expected to be
* space separated from the placement, i.e: 'auto bottom-left' If
* the primary and secondary placement values do not match 'top,
* bottom, left, right' then 'top' will be the primary placement and
* 'center' will be the secondary placement. If 'auto' is passed, true
* will be returned as the 3rd value of the array.
*
* @param {string} placement - The placement string to parse.
*
* @returns {array} An array with the following values
*
* - **[0]**: The primary placement.
* - **[1]**: The secondary placement.
* - **[2]**: If auto is passed: true, else undefined.
*
*/
parsePlacement: function(placement) {
var autoPlace = PLACEMENT_REGEX.auto.test(placement);
if (autoPlace) {
placement = placement.replace(PLACEMENT_REGEX.auto, '');
}
placement = placement.split('-');
placement[0] = placement[0] || 'top';
if (!PLACEMENT_REGEX.primary.test(placement[0])) {
placement[0] = 'top';
}
placement[1] = placement[1] || 'center';
if (!PLACEMENT_REGEX.secondary.test(placement[1])) {
placement[1] = 'center';
}
if (autoPlace) {
placement[2] = true;
} else {
placement[2] = false;
}
return placement;
},
/**
* Provides coordinates for an element to be positioned relative to
* another element. Passing 'auto' as part of the placement parameter
* will enable smart placement - where the element fits. i.e:
* 'auto left-top' will check to see if there is enough space to the left
* of the hostElem to fit the targetElem, if not place right (same for secondary
* top placement). Available space is calculated using the viewportOffset
* function.
*
* @param {element} hostElem - The element to position against.
* @param {element} targetElem - The element to position.
* @param {string=} [placement=top] - The placement for the targetElem,
* default is 'top'. 'center' is assumed as secondary placement for
* 'top', 'left', 'right', and 'bottom' placements. Available placements are:
*
* - top
* - top-right
* - top-left
* - bottom
* - bottom-left
* - bottom-right
* - left
* - left-top
* - left-bottom
* - right
* - right-top
* - right-bottom
*
* @param {boolean=} [appendToBody=false] - Should the top and left values returned
* be calculated from the body element, default is false.
*
* @returns {object} An object with the following properties:
*
* - **top**: Value for targetElem top.
* - **left**: Value for targetElem left.
* - **placement**: The resolved placement.
*
*/
positionElements: function(hostElem, targetElem, placement, appendToBody) {
hostElem = this.getRawNode(hostElem);
targetElem = this.getRawNode(targetElem);
// need to read from prop to support tests.
var targetWidth = angular.isDefined(targetElem.offsetWidth) ? targetElem.offsetWidth : targetElem.prop('offsetWidth');
var targetHeight = angular.isDefined(targetElem.offsetHeight) ? targetElem.offsetHeight : targetElem.prop('offsetHeight');
placement = this.parsePlacement(placement);
var hostElemPos = appendToBody ? this.offset(hostElem) : this.position(hostElem);
var targetElemPos = {top: 0, left: 0, placement: ''};
if (placement[2]) {
var viewportOffset = this.viewportOffset(hostElem, appendToBody);
var targetElemStyle = $window.getComputedStyle(targetElem);
var adjustedSize = {
width: targetWidth + Math.round(Math.abs(this.parseStyle(targetElemStyle.marginLeft) + this.parseStyle(targetElemStyle.marginRight))),
height: targetHeight + Math.round(Math.abs(this.parseStyle(targetElemStyle.marginTop) + this.parseStyle(targetElemStyle.marginBottom)))
};
placement[0] = placement[0] === 'top' && adjustedSize.height > viewportOffset.top && adjustedSize.height <= viewportOffset.bottom ? 'bottom' :
placement[0] === 'bottom' && adjustedSize.height > viewportOffset.bottom && adjustedSize.height <= viewportOffset.top ? 'top' :
placement[0] === 'left' && adjustedSize.width > viewportOffset.left && adjustedSize.width <= viewportOffset.right ? 'right' :
placement[0] === 'right' && adjustedSize.width > viewportOffset.right && adjustedSize.width <= viewportOffset.left ? 'left' :
placement[0];
placement[1] = placement[1] === 'top' && adjustedSize.height - hostElemPos.height > viewportOffset.bottom && adjustedSize.height - hostElemPos.height <= viewportOffset.top ? 'bottom' :
placement[1] === 'bottom' && adjustedSize.height - hostElemPos.height > viewportOffset.top && adjustedSize.height - hostElemPos.height <= viewportOffset.bottom ? 'top' :
placement[1] === 'left' && adjustedSize.width - hostElemPos.width > viewportOffset.right && adjustedSize.width - hostElemPos.width <= viewportOffset.left ? 'right' :
placement[1] === 'right' && adjustedSize.width - hostElemPos.width > viewportOffset.left && adjustedSize.width - hostElemPos.width <= viewportOffset.right ? 'left' :
placement[1];
if (placement[1] === 'center') {
if (PLACEMENT_REGEX.vertical.test(placement[0])) {
var xOverflow = hostElemPos.width / 2 - targetWidth / 2;
if (viewportOffset.left + xOverflow < 0 && adjustedSize.width - hostElemPos.width <= viewportOffset.right) {
placement[1] = 'left';
} else if (viewportOffset.right + xOverflow < 0 && adjustedSize.width - hostElemPos.width <= viewportOffset.left) {
placement[1] = 'right';
}
} else {
var yOverflow = hostElemPos.height / 2 - adjustedSize.height / 2;
if (viewportOffset.top + yOverflow < 0 && adjustedSize.height - hostElemPos.height <= viewportOffset.bottom) {
placement[1] = 'top';
} else if (viewportOffset.bottom + yOverflow < 0 && adjustedSize.height - hostElemPos.height <= viewportOffset.top) {
placement[1] = 'bottom';
}
}
}
}
switch (placement[0]) {
case 'top':
targetElemPos.top = hostElemPos.top - targetHeight;
break;
case 'bottom':
targetElemPos.top = hostElemPos.top + hostElemPos.height;
break;
case 'left':
targetElemPos.left = hostElemPos.left - targetWidth;
break;
case 'right':
targetElemPos.left = hostElemPos.left + hostElemPos.width;
break;
}
switch (placement[1]) {
case 'top':
targetElemPos.top = hostElemPos.top;
break;
case 'bottom':
targetElemPos.top = hostElemPos.top + hostElemPos.height - targetHeight;
break;
case 'left':
targetElemPos.left = hostElemPos.left;
break;
case 'right':
targetElemPos.left = hostElemPos.left + hostElemPos.width - targetWidth;
break;
case 'center':
if (PLACEMENT_REGEX.vertical.test(placement[0])) {
targetElemPos.left = hostElemPos.left + hostElemPos.width / 2 - targetWidth / 2;
} else {
targetElemPos.top = hostElemPos.top + hostElemPos.height / 2 - targetHeight / 2;
}
break;
}
targetElemPos.top = Math.round(targetElemPos.top);
targetElemPos.left = Math.round(targetElemPos.left);
targetElemPos.placement = placement[1] === 'center' ? placement[0] : placement[0] + '-' + placement[1];
return targetElemPos;
},
/**
* Provides a way to adjust the top positioning after first
* render to correctly align element to top after content
* rendering causes resized element height
*
* @param {array} placementClasses - The array of strings of classes
* element should have.
* @param {object} containerPosition - The object with container
* position information
* @param {number} initialHeight - The initial height for the elem.
* @param {number} currentHeight - The current height for the elem.
*/
adjustTop: function(placementClasses, containerPosition, initialHeight, currentHeight) {
if (placementClasses.indexOf('top') !== -1 && initialHeight !== currentHeight) {
return {
top: containerPosition.top - currentHeight + 'px'
};
}
},
/**
* Provides a way for positioning tooltip & dropdown
* arrows when using placement options beyond the standard
* left, right, top, or bottom.
*
* @param {element} elem - The tooltip/dropdown element.
* @param {string} placement - The placement for the elem.
*/
positionArrow: function(elem, placement) {
elem = this.getRawNode(elem);
var innerElem = elem.querySelector('.tooltip-inner, .popover-inner');
if (!innerElem) {
return;
}
var isTooltip = angular.element(innerElem).hasClass('tooltip-inner');
var arrowElem = isTooltip ? elem.querySelector('.tooltip-arrow') : elem.querySelector('.arrow');
if (!arrowElem) {
return;
}
var arrowCss = {
top: '',
bottom: '',
left: '',
right: ''
};
placement = this.parsePlacement(placement);
if (placement[1] === 'center') {
// no adjustment necessary - just reset styles
angular.element(arrowElem).css(arrowCss);
return;
}
var borderProp = 'border-' + placement[0] + '-width';
var borderWidth = $window.getComputedStyle(arrowElem)[borderProp];
var borderRadiusProp = 'border-';
if (PLACEMENT_REGEX.vertical.test(placement[0])) {
borderRadiusProp += placement[0] + '-' + placement[1];
} else {
borderRadiusProp += placement[1] + '-' + placement[0];
}
borderRadiusProp += '-radius';
var borderRadius = $window.getComputedStyle(isTooltip ? innerElem : elem)[borderRadiusProp];
switch (placement[0]) {
case 'top':
arrowCss.bottom = isTooltip ? '0' : '-' + borderWidth;
break;
case 'bottom':
arrowCss.top = isTooltip ? '0' : '-' + borderWidth;
break;
case 'left':
arrowCss.right = isTooltip ? '0' : '-' + borderWidth;
break;
case 'right':
arrowCss.left = isTooltip ? '0' : '-' + borderWidth;
break;
}
arrowCss[placement[1]] = borderRadius;
angular.element(arrowElem).css(arrowCss);
}
};
}]);
================================================
FILE: src/position/test/position.spec.js
================================================
describe('$uibPosition service', function () {
var TargetElMock = function(width, height) {
this.width = width;
this.height = height;
this.prop = function(propName) {
return propName === 'offsetWidth' ? width : height;
};
};
var $document;
var $uibPosition;
beforeEach(module('ui.bootstrap.position'));
beforeEach(inject(function(_$document_, _$uibPosition_) {
$document = _$document_;
$uibPosition = _$uibPosition_;
}));
beforeEach(function () {
jasmine.addMatchers({
toBePositionedAt: function(util, customEqualityTesters) {
return {
compare: function(actual, top, left) {
var result = {
pass: util.equals(actual.top, top, customEqualityTesters) &&
util.equals(actual.left, left, customEqualityTesters)
};
if (result.pass) {
result.message = 'Expected "(' + actual.top + ', ' + actual.left + ')" not to be positioned at (' + top + ', ' + left + ')';
} else {
result.message = 'Expected "(' + actual.top + ', ' + actual.left + ')" to be positioned at (' + top + ', ' + left + ')';
}
return result;
}
};
}
});
});
describe('rawnode', function() {
it('returns the raw DOM element from an angular element', function() {
var angularEl = angular.element('
');
var el = $uibPosition.getRawNode(angularEl);
expect(el.nodeName).toBe('DIV');
});
it('returns the raw DOM element from a select element', function() {
var angularEl = angular.element('
');
var el = $uibPosition.getRawNode(angularEl);
expect(el.nodeName).toBe('SELECT');
});
});
describe('offset', function() {
it('returns getBoundingClientRect by default', function() {
var el = angular.element('
Foo
');
/* getBoundingClientRect values will be based on the testing Chrome window
so that makes this tests very brittle if we don't mock */
spyOn(el[0], 'getBoundingClientRect').and.returnValue({
width: 100,
height: 100,
top: 2,
left: 2
});
$document.find('body').append(el);
var offset = $uibPosition.offset(el);
expect(offset).toEqual({
width: 100,
height: 100,
top: 2,
left: 2
});
el.remove();
});
});
describe('viewportOffset', function() {
var el;
beforeEach(function() {
el = angular.element('
');
$document.find('body').append(el);
});
afterEach(function() {
el.remove();
});
it('measures the offset', function() {
var vpOffset = $uibPosition.viewportOffset(document.getElementById('inner'));
expect(vpOffset).toEqual({
top: 20,
bottom: 30,
left: 20,
right: 30
});
});
it('measures the offset without padding', function() {
var outerEl = document.getElementById('outer');
outerEl.style.paddingTop = '0px';
outerEl.style.paddingBottom = '0px';
outerEl.style.paddingLeft = '0px';
outerEl.style.paddingRight = '0px';
var vpOffset = $uibPosition.viewportOffset(document.getElementById('inner'));
expect(vpOffset).toEqual({
top: 20,
bottom: 80,
left: 20,
right: 80
});
});
it('measures the offset with borders', function() {
var outerEl = document.getElementById('outer');
outerEl.style.width = '220px';
outerEl.style.height = '220px';
outerEl.style.border = '10px solid black';
var vpOffset = $uibPosition.viewportOffset(document.getElementById('inner'));
expect(vpOffset).toEqual({
top: 20,
bottom: 30,
left: 20,
right: 30
});
});
it('measures the offset excluding padding', function() {
var vpOffset = $uibPosition.viewportOffset(document.getElementById('inner'), false, false);
expect(vpOffset).toEqual({
top: 45,
bottom: 55,
left: 45,
right: 55
});
});
it('measures the offset when scrolled', function() {
var innerEl = document.getElementById('inner');
innerEl.style.width = '300px';
innerEl.style.height = '300px';
var outerEl = document.getElementById('outer');
outerEl.scrollTop = 25;
outerEl.scrollLeft = 25;
var vpOffset = $uibPosition.viewportOffset(document.getElementById('inner'));
expect(vpOffset.top).toEqual(-5);
expect(vpOffset.bottom).toBeGreaterThan(-180);
expect(vpOffset.left).toEqual(-5);
expect(vpOffset.right).toBeGreaterThan(-180);
//brittle
// expect(vpOffset).toEqual({
// top: -5,
// bottom: -162,
// left: -5,
// right: -162
// });
});
});
describe('position', function() {
var el;
afterEach(function() {
el.remove();
});
it('gets position with document as the relative parent', function() {
el = angular.element('
Foo
');
spyOn(el[0], 'getBoundingClientRect').and.returnValue({
width: 100,
height: 100,
top: 2,
left: 2
});
$document.find('body').append(el);
var position = $uibPosition.position(el);
expect(position).toEqual({
width: 100,
height: 100,
top: 2,
left: 2
});
});
it('gets position with an element as the relative parent', function() {
el = angular.element('
');
$document.find('body').append(el);
var outerEl = angular.element(document.getElementById('outer'));
var innerEl = angular.element(document.getElementById('inner'));
spyOn(outerEl[0], 'getBoundingClientRect').and.returnValue({
width: 100,
height: 100,
top: 2,
left: 2
});
spyOn(innerEl[0], 'getBoundingClientRect').and.returnValue({
width: 20,
height: 20,
top: 5,
left: 5
});
var position = $uibPosition.position(innerEl);
expect(position).toEqual({
width: 20,
height: 20,
top: 3,
left: 3
});
});
});
describe('isScrollable', function() {
var el;
afterEach(function() {
el.remove();
});
it('should return true if the element is scrollable', function() {
el = angular.element('
');
$document.find('body').append(el);
expect($uibPosition.isScrollable(el)).toBe(true);
});
it('should return false if the element is scrollable', function() {
el = angular.element('
');
$document.find('body').append(el);
expect($uibPosition.isScrollable(el)).toBe(false);
});
});
describe('scrollParent', function() {
var el;
afterEach(function() {
el.remove();
});
it('gets the closest scrollable ancestor', function() {
el = angular.element('
');
$document.find('body').css({overflow: 'auto'}).append(el);
var outerEl = document.getElementById('outer');
var innerEl = document.getElementById('inner');
var scrollParent = $uibPosition.scrollParent(innerEl);
expect(scrollParent).toEqual(outerEl);
});
it('gets the closest scrollable ancestor with overflow-x: scroll', function() {
el = angular.element('
');
$document.find('body').css({overflow: 'auto'}).append(el);
var outerEl = document.getElementById('outer');
var innerEl = document.getElementById('inner');
var scrollParent = $uibPosition.scrollParent(innerEl);
expect(scrollParent).toEqual(outerEl);
});
it('gets the closest scrollable ancestor with overflow-y: hidden', function() {
el = angular.element('
');
$document.find('body').css({overflow: 'auto'}).append(el);
var outerEl = document.getElementById('outer');
var innerEl = document.getElementById('inner');
var scrollParent = $uibPosition.scrollParent(innerEl, true);
expect(scrollParent).toEqual(outerEl);
});
it('gets the document element if no scrollable ancestor exists', function() {
el = angular.element('
');
$document.find('body').css({overflow: ''}).append(el);
var innerEl = document.getElementById('inner');
var scrollParent = $uibPosition.scrollParent(innerEl);
expect(scrollParent).toEqual($document[0].documentElement);
});
it('gets the closest scrollable ancestor after a positioned ancestor when positioned absolute', function() {
el = angular.element('
');
$document.find('body').css({overflow: 'auto'}).append(el);
var outerEl = document.getElementById('outer');
var innerEl = document.getElementById('inner');
var scrollParent = $uibPosition.scrollParent(innerEl);
expect(scrollParent).toEqual(outerEl);
});
});
describe('positionElements - append-to-body: false', function() {
var el;
beforeEach(function() {
//mock position info normally queried from the DOM
$uibPosition.position = function() {
return {
width: 20,
height: 20,
top: 100,
left: 100
};
};
});
it('should position element on top-center by default', function() {
expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'other')).toBePositionedAt(90, 105);
expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'top')).toBePositionedAt(90, 105);
expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'top-center')).toBePositionedAt(90, 105);
});
it('should position on top-left', function() {
expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'top-left')).toBePositionedAt(90, 100);
});
it('should position on top-right', function() {
expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'top-right')).toBePositionedAt(90, 110);
});
it('should position elements on bottom-center when "bottom" specified', function() {
expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'bottom')).toBePositionedAt(120, 105);
expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'bottom-center')).toBePositionedAt(120, 105);
});
it('should position elements on bottom-left', function() {
expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'bottom-left')).toBePositionedAt(120, 100);
});
it('should position elements on bottom-right', function() {
expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'bottom-right')).toBePositionedAt(120, 110);
});
it('should position elements on left-center when "left" specified', function() {
expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'left')).toBePositionedAt(105, 90);
expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'left-center')).toBePositionedAt(105, 90);
});
it('should position elements on left-top when "left-top" specified', function() {
expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'left-top')).toBePositionedAt(100, 90);
});
it('should position elements on left-bottom when "left-bottom" specified', function() {
expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'left-bottom')).toBePositionedAt(110, 90);
});
it('should position elements on right-center when "right" specified', function() {
expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'right')).toBePositionedAt(105, 120);
expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'right-center')).toBePositionedAt(105, 120);
});
it('should position elements on right-top when "right-top" specified', function() {
expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'right-top')).toBePositionedAt(100, 120);
});
it('should position elements on right-top when "right-top" specified', function() {
expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'right-bottom')).toBePositionedAt(110, 120);
});
});
describe('positionElements - append-to-body: true', function() {
beforeEach(function() {
//mock offset info normally queried from the DOM
$uibPosition.offset = function() {
return {
width: 20,
height: 20,
top: 100,
left: 100
};
};
});
it('should position element on top-center by default', function() {
expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'other', true)).toBePositionedAt(90, 105);
expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'top', true)).toBePositionedAt(90, 105);
expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'top-center', true)).toBePositionedAt(90, 105);
});
it('should position on top-left', function() {
expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'top-left', true)).toBePositionedAt(90, 100);
});
it('should position on top-right', function() {
expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'top-right', true)).toBePositionedAt(90, 110);
});
it('should position elements on bottom-center when "bottom" specified', function() {
expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'bottom', true)).toBePositionedAt(120, 105);
expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'bottom-center', true)).toBePositionedAt(120, 105);
});
it('should position elements on bottom-left', function() {
expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'bottom-left', true)).toBePositionedAt(120, 100);
});
it('should position elements on bottom-right', function() {
expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'bottom-right', true)).toBePositionedAt(120, 110);
});
it('should position elements on left-center when "left" specified', function() {
expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'left', true)).toBePositionedAt(105, 90);
expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'left-center', true)).toBePositionedAt(105, 90);
});
it('should position elements on left-top when "left-top" specified', function() {
expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'left-top', true)).toBePositionedAt(100, 90);
});
it('should position elements on left-bottom when "left-bottom" specified', function() {
expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'left-bottom', true)).toBePositionedAt(110, 90);
});
it('should position elements on right-center when "right" specified', function() {
expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'right', true)).toBePositionedAt(105, 120);
expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'right-center', true)).toBePositionedAt(105, 120);
});
it('should position elements on right-top when "right-top" specified', function() {
expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'right-top', true)).toBePositionedAt(100, 120);
});
it('should position elements on right-bottom when "right-bottom" specified', function() {
expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'right-bottom', true)).toBePositionedAt(110, 120);
});
});
describe('smart positioning', function() {
var viewportOffset, el;
beforeEach(function() {
el = angular.element('
');
$document.find('body').append(el);
//mock position info normally queried from the DOM
$uibPosition.position = function() {
return {
width: 40,
height: 40,
top: 100,
left: 100
};
};
viewportOffset = {
width: 10,
height: 10,
top: 10,
bottom: 10,
left: 10,
right: 10
};
$uibPosition.viewportOffset = function() {
return viewportOffset;
};
});
afterEach(function() {
el.remove();
});
// tests primary top -> bottom
// tests secondary left -> right
it('should position element on bottom-right when top-left does not fit', function() {
viewportOffset.bottom = 20;
viewportOffset.left = 20;
el.css({ width: '60px', height: '20px' });
expect($uibPosition.positionElements({}, el, 'auto top-left')).toBePositionedAt(140, 80);
});
// tests primary bottom -> top
// tests secondary right -> left
it('should position element on top-left when bottom-right does not fit', function() {
viewportOffset.top = 20;
viewportOffset.right = 20;
el.css({ width: '60px', height: '20px' });
expect($uibPosition.positionElements({}, el, 'auto bottom-right')).toBePositionedAt(80, 100);
});
// tests primary left -> right
// tests secondary top -> bottom
it('should position element on right-bottom when left-top does not fit', function() {
viewportOffset.top = 20;
viewportOffset.right = 20;
el.css({ width: '20px', height: '60px' });
expect($uibPosition.positionElements({}, el, 'auto left-top')).toBePositionedAt(80, 140);
});
// tests primary right -> left
// tests secondary bottom -> top
it('should position element on left-top when right-bottom does not fit', function() {
viewportOffset.bottom = 20;
viewportOffset.left = 20;
el.css({ width: '20px', height: '60px' });
expect($uibPosition.positionElements({}, el, 'auto right-bottom')).toBePositionedAt(100, 80);
});
// tests vertical center -> top
it('should position element on left-top when left-center does not fit vetically', function() {
viewportOffset.bottom = 100;
el.css({ width: '20px', height: '120px' });
expect($uibPosition.positionElements({}, el, 'auto left')).toBePositionedAt(100, 80);
});
// tests vertical center -> bottom
it('should position element on left-bottom when left-center does not fit vertically', function() {
viewportOffset.top = 100;
el.css({ width: '20px', height: '120px' });
expect($uibPosition.positionElements({}, el, 'auto left')).toBePositionedAt(20, 80);
});
// tests horizontal center -> left
it('should position element on top-left when top-center does not fit horizontally', function() {
viewportOffset.right = 100;
el.css({ width: '120px', height: '20px' });
expect($uibPosition.positionElements({}, el, 'auto top')).toBePositionedAt(80, 100);
});
// tests horizontal center -> right
it('should position element on top-right when top-center does not fit horizontally', function() {
viewportOffset.left = 100;
el.css({ width: '120px', height: '20px' });
expect($uibPosition.positionElements({}, el, 'auto top')).toBePositionedAt(80, 20);
});
});
});
================================================
FILE: src/position/test/test.html
================================================
Within body
Content
Within statically positioned DIV
Within relative-positioned DIV - position specified in CSS
Within relative-positioned DIV
Within absolute-positioned DIV
Within overflowing absolute-positioned DIV
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur non velit nulla. Suspendisse sit amet tempus diam. Sed at ultricies neque. Suspendisse id felis a sem placerat ornare. Donec auctor, purus at molestie tempor, arcu enim molestie lacus, ac imperdiet massa urna eu massa. Praesent velit tellus, scelerisque a fermentum ut, ornare in diam. Phasellus egestas molestie feugiat. Vivamus sit amet viverra metus.
Content absolute overflow
Next to a float element
Within a table
| Some other content |
Content
|
Within a table that is inside a relative-positioned DIV
| Some other content |
Content
|
Inside svg
Inside looong text
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur non velit nulla. Suspendisse sit amet tempus diam. Sed at ultricies neque. Suspendisse id felis a sem placerat ornare. Donec auctor, purus at molestie tempor, arcu enim molestie lacus, ac imperdiet massa urna eu massa. Praesent velit tellus, scelerisque a fermentum ut, ornare in diam. Phasellus egestas molestie feugiat. Vivamus sit amet viverra metus.
Etiam ultricies odio commodo erat ullamcorper sodales. Nullam ac dui ac libero dictum mollis. Quisque convallis adipiscing facilisis. In nec nisi velit, id auctor lectus. Cras interdum urna non felis lacinia vulputate. Integer dignissim, mi aliquam gravida auctor, massa odio cursus lorem, eu ultrices eros nisl tempus diam. Maecenas tristique pellentesque nisi sed adipiscing. Aenean hendrerit sapien quis arcu lobortis vitae pulvinar ante volutpat. Morbi consectetur erat eu lacus facilisis eu ullamcorper orci euismod. Quisque diam dui, interdum in suscipit et, fringilla non justo. Pellentesque non nibh odio. Proin sit amet massa sem.
Nam in urna erat, at congue nisi. Donec eu tellus lorem, sed facilisis tellus. Aliquam suscipit faucibus ipsum, at hendrerit metus interdum at. Integer et eros ac lacus vulputate sagittis quis quis erat. Suspendisse consectetur vehicula purus vitae imperdiet. Suspendisse in augue magna, quis imperdiet enim. Nullam non diam ac erat auctor bibendum. Praesent ante mauris, egestas sit amet molestie sed, tristique at lorem. Nam at mi ac nisl venenatis semper nec eget mi. Pellentesque a lectus ac leo feugiat suscipit. Quisque tristique dui nec urna placerat a viverra mi iaculis. Ut et tellus et turpis sagittis iaculis nec eu magna. Sed quis nunc non arcu tincidunt ultricies viverra id mauris.
Curabitur luctus rutrum ultricies. Aenean ut rutrum orci. Sed molestie lorem in leo cursus id feugiat nisi scelerisque. Maecenas pulvinar neque nec lacus feugiat dictum. Donec viverra felis nec nisi mollis feugiat. Phasellus vehicula, ligula at mattis porttitor, sapien urna hendrerit quam, at fringilla nisl quam vel elit. In eu lacus ligula. Praesent eget gravida nisl. Suspendisse velit diam, pellentesque a tempus quis, vestibulum vel leo.
Maecenas feugiat ultrices laoreet. Sed congue posuere diam ac faucibus. Pellentesque eget leo ligula. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Sed nec quam eu tellus sagittis cursus a sit amet eros. Mauris sit amet orci at orci vulputate commodo ut ut nunc. Etiam sagittis erat ut nisi ultricies feugiat. Morbi sed eros nisi. Cras vitae augue in risus aliquet commodo non id est.
HERE
Maecenas laoreet nisi pretium elit bibendum eget tempor nunc aliquet. Vivamus interdum nisi sit amet tortor fermentum congue. Suspendisse at posuere erat. Aliquam hendrerit ultricies nunc non adipiscing. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Duis molestie viverra nulla a aliquet. Nullam non eros vel sem vehicula suscipit. Ut sit amet arcu ac tortor dignissim viverra in a ligula.
================================================
FILE: src/progressbar/docs/demo.html
================================================
Static
Dynamic
{{dynamic}} / {{max}}
No animation
{{dynamic}}%
Object (changes type based on value)
{{type}} !!! Watch out !!!
Stacked
{{bar.value}}%
================================================
FILE: src/progressbar/docs/demo.js
================================================
angular.module('ui.bootstrap.demo').controller('ProgressDemoCtrl', function ($scope) {
$scope.max = 200;
$scope.random = function() {
var value = Math.floor(Math.random() * 100 + 1);
var type;
if (value < 25) {
type = 'success';
} else if (value < 50) {
type = 'info';
} else if (value < 75) {
type = 'warning';
} else {
type = 'danger';
}
$scope.showWarning = type === 'danger' || type === 'warning';
$scope.dynamic = value;
$scope.type = type;
};
$scope.random();
$scope.randomStacked = function() {
$scope.stacked = [];
var types = ['success', 'info', 'warning', 'danger'];
for (var i = 0, n = Math.floor(Math.random() * 4 + 1); i < n; i++) {
var index = Math.floor(Math.random() * 4);
$scope.stacked.push({
value: Math.floor(Math.random() * 30 + 1),
type: types[index]
});
}
};
$scope.randomStacked();
});
================================================
FILE: src/progressbar/docs/readme.md
================================================
A progress bar directive that is focused on providing feedback on the progress of a workflow or action.
It supports multiple (stacked) `
` into the same `` element or a single `` element with optional `max` attribute and transition animations.
### uib-progressbar settings
* `value`
$
-
The current value of progress completed.
* `type`
_(Default: `null`)_ -
Bootstrap style type. Possible values are 'success', 'info', 'warning', and, 'danger' to use Bootstrap's pre-existing styling, or any desired custom suffix.
* `max`
$
C
_(Default: `100`)_ -
A number that specifies the total value of bars that is required.
* `animate`
$
C
_(Default: `true`)_ -
Whether bars use transitions to achieve the width change.
* `title`
_(Default: `progressbar`)_ -
Title to use as label (for accessibility).
### uib-progress settings
* `max`
$
C
_(Default: `100`)_ -
A number that specifies the total value of bars that is required.
* `animate`
$
C
_(Default: `true`)_ -
Whether bars use transitions to achieve the width change.
* `title`
_(Default: `progressbar`)_ -
Title to use as label (for accessibility).
### uib-bar settings
* `value`
$
-
The current value of progress completed.
* `type`
_(Default: `null`)_ -
Bootstrap style type. Possible values are 'success', 'info', 'warning', and, 'danger' to use Bootstrap's pre-existing styling, or any desired custom suffix.
* `title`
_(Default: `progressbar`)_ -
Title to use as label (for accessibility).
================================================
FILE: src/progressbar/index.js
================================================
require('../../template/progressbar/progressbar.html.js');
require('../../template/progressbar/progress.html.js');
require('../../template/progressbar/bar.html.js');
require('./progressbar');
var MODULE_NAME = 'ui.bootstrap.module.progressbar';
angular.module(MODULE_NAME, ['ui.bootstrap.progressbar', 'uib/template/progressbar/progressbar.html', 'uib/template/progressbar/progress.html', 'uib/template/progressbar/bar.html']);
module.exports = MODULE_NAME;
================================================
FILE: src/progressbar/progressbar.js
================================================
angular.module('ui.bootstrap.progressbar', [])
.constant('uibProgressConfig', {
animate: true,
max: 100
})
.controller('UibProgressController', ['$scope', '$attrs', 'uibProgressConfig', function($scope, $attrs, progressConfig) {
var self = this,
animate = angular.isDefined($attrs.animate) ? $scope.$parent.$eval($attrs.animate) : progressConfig.animate;
this.bars = [];
$scope.max = getMaxOrDefault();
this.addBar = function(bar, element, attrs) {
if (!animate) {
element.css({'transition': 'none'});
}
this.bars.push(bar);
bar.max = getMaxOrDefault();
bar.title = attrs && angular.isDefined(attrs.title) ? attrs.title : 'progressbar';
bar.$watch('value', function(value) {
bar.recalculatePercentage();
});
bar.recalculatePercentage = function() {
var totalPercentage = self.bars.reduce(function(total, bar) {
bar.percent = +(100 * bar.value / bar.max).toFixed(2);
return total + bar.percent;
}, 0);
if (totalPercentage > 100) {
bar.percent -= totalPercentage - 100;
}
};
bar.$on('$destroy', function() {
element = null;
self.removeBar(bar);
});
};
this.removeBar = function(bar) {
this.bars.splice(this.bars.indexOf(bar), 1);
this.bars.forEach(function (bar) {
bar.recalculatePercentage();
});
};
//$attrs.$observe('maxParam', function(maxParam) {
$scope.$watch('maxParam', function(maxParam) {
self.bars.forEach(function(bar) {
bar.max = getMaxOrDefault();
bar.recalculatePercentage();
});
});
function getMaxOrDefault () {
return angular.isDefined($scope.maxParam) ? $scope.maxParam : progressConfig.max;
}
}])
.directive('uibProgress', function() {
return {
replace: true,
transclude: true,
controller: 'UibProgressController',
require: 'uibProgress',
scope: {
maxParam: '=?max'
},
templateUrl: 'uib/template/progressbar/progress.html'
};
})
.directive('uibBar', function() {
return {
replace: true,
transclude: true,
require: '^uibProgress',
scope: {
value: '=',
type: '@'
},
templateUrl: 'uib/template/progressbar/bar.html',
link: function(scope, element, attrs, progressCtrl) {
progressCtrl.addBar(scope, element, attrs);
}
};
})
.directive('uibProgressbar', function() {
return {
replace: true,
transclude: true,
controller: 'UibProgressController',
scope: {
value: '=',
maxParam: '=?max',
type: '@'
},
templateUrl: 'uib/template/progressbar/progressbar.html',
link: function(scope, element, attrs, progressCtrl) {
progressCtrl.addBar(scope, angular.element(element.children()[0]), {title: attrs.title});
}
};
});
================================================
FILE: src/progressbar/test/progressbar.spec.js
================================================
describe('progressbar directive', function() {
var $rootScope, $compile, element;
beforeEach(module('ui.bootstrap.progressbar'));
beforeEach(module('uib/template/progressbar/progressbar.html', 'uib/template/progressbar/progress.html', 'uib/template/progressbar/bar.html'));
beforeEach(inject(function(_$compile_, _$rootScope_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
$rootScope.value = 22;
element = $compile('{{value}} %')($rootScope);
$rootScope.$digest();
}));
var BAR_CLASS = 'progress-bar';
function getBar(i) {
return element.children().eq(i);
}
it('has a "progress" css class', function() {
expect(element).toHaveClass('progress');
});
it('contains one child element with "bar" css class', function() {
expect(element.children().length).toBe(1);
expect(getBar(0)).toHaveClass(BAR_CLASS);
});
it('has a "bar" element with expected width', function() {
expect(getBar(0).css('width')).toBe('22%');
});
it('has the appropriate aria markup', function() {
var bar = getBar(0);
expect(bar.attr('role')).toBe('progressbar');
expect(bar.attr('aria-valuemin')).toBe('0');
expect(bar.attr('aria-valuemax')).toBe('100');
expect(bar.attr('aria-valuenow')).toBe('22');
expect(bar.attr('aria-valuetext')).toBe('22%');
expect(bar.attr('aria-labelledby')).toBe('foo');
});
it('has the default aria-labelledby value of `progressbar`', function() {
element = $compile('{{value}} %')($rootScope);
$rootScope.$digest();
var bar = getBar(0);
expect(bar.attr('aria-labelledby')).toBe('progressbar');
});
it('transcludes "bar" text', function() {
expect(getBar(0).text()).toBe('22 %');
});
it('it should be possible to add additional classes', function() {
element = $compile('')($rootScope);
$rootScope.$digest();
expect(element).toHaveClass('progress-striped');
expect(element).toHaveClass('active');
expect(getBar(0)).toHaveClass('pizza');
});
it('adjusts the "bar" width and aria when value changes', function() {
$rootScope.value = 60;
$rootScope.$digest();
var bar = getBar(0);
expect(bar.css('width')).toBe('60%');
expect(bar.attr('aria-valuemin')).toBe('0');
expect(bar.attr('aria-valuemax')).toBe('100');
expect(bar.attr('aria-valuenow')).toBe('60');
expect(bar.attr('aria-valuetext')).toBe('60%');
});
it('allows fractional "bar" width values, rounded to two places', function() {
$rootScope.value = 5.625;
$rootScope.$digest();
expect(getBar(0).css('width')).toBe('5.63%');
$rootScope.value = 1.3;
$rootScope.$digest();
expect(getBar(0).css('width')).toBe('1.3%');
});
it('does not include decimals in aria values', function() {
$rootScope.value = 50.34;
$rootScope.$digest();
var bar = getBar(0);
expect(bar.css('width')).toBe('50.34%');
expect(bar.attr('aria-valuetext')).toBe('50%');
});
describe('"max" attribute', function() {
beforeEach(inject(function() {
$rootScope.max = 200;
element = $compile('{{value}}/{{max}}')($rootScope);
$rootScope.$digest();
}));
it('has the appropriate aria markup', function() {
expect(getBar(0).attr('aria-valuemax')).toBe('200');
});
it('adjusts the "bar" width', function() {
expect(element.children().eq(0).css('width')).toBe('11%');
});
it('adjusts the "bar" width when value changes', function() {
$rootScope.value = 60;
$rootScope.$digest();
expect(getBar(0).css('width')).toBe('30%');
$rootScope.value += 12;
$rootScope.$digest();
expect(getBar(0).css('width')).toBe('36%');
$rootScope.value = 0;
$rootScope.$digest();
expect(getBar(0).css('width')).toBe('0%');
});
it('transcludes "bar" text', function() {
expect(getBar(0).text()).toBe('22/200');
});
it('adjusts the valuemax when it changes', function() {
expect(getBar(0).attr('aria-valuemax')).toBe('200');
$rootScope.max = 300;
$rootScope.$digest();
expect(getBar(0).attr('aria-valuemax')).toBe('300');
});
});
describe('"max" attribute using object', function() {
beforeEach(inject(function() {
element = $compile('{{settings.value}}/{{settings.max}}')($rootScope);
$rootScope.$digest();
}));
it('should not modify outside object', function() {
if (typeof $rootScope.settings === 'object') {
// angular set's up the nested object therefore we have to check like this to avoid test crash
expect($rootScope.settings.max).toBeUndefined();
}
expect($rootScope.settings).toBeUndefined();
expect(getBar(0).attr('aria-valuemax')).toBe('100');
$rootScope.settings = {
max: 300,
value: 40
};
$rootScope.$digest();
expect($rootScope.settings.max).toBe(300);
expect(getBar(0).attr('aria-valuemax')).toBe('300');
});
});
describe('"type" attribute', function() {
beforeEach(inject(function() {
$rootScope.type = 'success';
element = $compile('')($rootScope);
$rootScope.$digest();
}));
it('should use correct classes', function() {
expect(getBar(0)).toHaveClass(BAR_CLASS);
expect(getBar(0)).toHaveClass(BAR_CLASS + '-success');
});
it('should change classes if type changed', function() {
$rootScope.type = 'warning';
$rootScope.value += 1;
$rootScope.$digest();
var barEl = getBar(0);
expect(barEl).toHaveClass(BAR_CLASS);
expect(barEl).not.toHaveClass(BAR_CLASS + '-success');
expect(barEl).toHaveClass(BAR_CLASS + '-warning');
});
});
describe('stacked', function() {
beforeEach(inject(function() {
$rootScope.objects = [
{ value: 10, title: 'foo', type: 'success' },
{ value: 50, title: 'bar', type: 'warning' },
{ value: 20, title: 'baz' }
];
element = $compile('{{o.value}}')($rootScope);
$rootScope.$digest();
}));
it('contains the right number of bars', function() {
expect(element.children().length).toBe(3);
for (var i = 0; i < 3; i++) {
expect(getBar(i)).toHaveClass(BAR_CLASS);
}
});
it('renders each bar with the appropriate width', function() {
expect(getBar(0).css('width')).toBe('10%');
expect(getBar(1).css('width')).toBe('50%');
expect(getBar(2).css('width')).toBe('20%');
});
it('uses correct classes', function() {
expect(getBar(0)).toHaveClass(BAR_CLASS + '-success');
expect(getBar(0)).not.toHaveClass(BAR_CLASS + '-warning');
expect(getBar(1)).not.toHaveClass(BAR_CLASS + '-success');
expect(getBar(1)).toHaveClass(BAR_CLASS + '-warning');
expect(getBar(2)).not.toHaveClass(BAR_CLASS + '-success');
expect(getBar(2)).not.toHaveClass(BAR_CLASS + '-warning');
});
it('should change classes if type changed', function() {
$rootScope.objects = [
{ value: 20, type: 'warning' },
{ value: 50 },
{ value: 30, type: 'info' }
];
$rootScope.$digest();
expect(getBar(0)).not.toHaveClass(BAR_CLASS + '-success');
expect(getBar(0)).toHaveClass(BAR_CLASS + '-warning');
expect(getBar(1)).not.toHaveClass(BAR_CLASS + '-success');
expect(getBar(1)).not.toHaveClass(BAR_CLASS + '-warning');
expect(getBar(2)).toHaveClass(BAR_CLASS + '-info');
expect(getBar(2)).not.toHaveClass(BAR_CLASS + '-success');
expect(getBar(2)).not.toHaveClass(BAR_CLASS + '-warning');
});
it('should change classes if type changed', function() {
$rootScope.objects = [
{ value: 70, type: 'info' }
];
$rootScope.$digest();
expect(element.children().length).toBe(1);
expect(getBar(0)).toHaveClass(BAR_CLASS + '-info');
expect(getBar(0)).not.toHaveClass(BAR_CLASS + '-success');
expect(getBar(0)).not.toHaveClass(BAR_CLASS + '-warning');
});
it('should have the correct aria markup', function() {
expect(getBar(0).attr('aria-valuenow')).toBe('10');
expect(getBar(0).attr('aria-valuemin')).toBe('0');
expect(getBar(0).attr('aria-valuemax')).toBe('100');
expect(getBar(0).attr('aria-valuetext')).toBe('10%');
expect(getBar(0).attr('aria-labelledby')).toBe('foo');
expect(getBar(1).attr('aria-valuenow')).toBe('50');
expect(getBar(1).attr('aria-valuemin')).toBe('0');
expect(getBar(1).attr('aria-valuemax')).toBe('100');
expect(getBar(1).attr('aria-valuetext')).toBe('50%');
expect(getBar(1).attr('aria-labelledby')).toBe('bar');
expect(getBar(2).attr('aria-valuenow')).toBe('20');
expect(getBar(2).attr('aria-valuemin')).toBe('0');
expect(getBar(2).attr('aria-valuemax')).toBe('100');
expect(getBar(2).attr('aria-valuetext')).toBe('20%');
expect(getBar(2).attr('aria-labelledby')).toBe('baz');
});
it('should default to `progressbar`', function() {
$rootScope.objects = [
{ value: 10, title: 'foo', type: 'success' },
{ value: 50, title: 'bar', type: 'warning' },
{ value: 20, title: 'baz' }
];
element = $compile('{{o.value}}')($rootScope);
$rootScope.$digest();
expect(getBar(0).attr('aria-labelledby')).toBe('progressbar');
expect(getBar(1).attr('aria-labelledby')).toBe('progressbar');
expect(getBar(2).attr('aria-labelledby')).toBe('progressbar');
});
describe('"max" attribute', function() {
beforeEach(inject(function() {
$rootScope.max = 200;
element = $compile('{{o.value}}/{{max}}')($rootScope);
$rootScope.$digest();
}));
it('has the appropriate aria markup', function() {
expect(getBar(0).attr('aria-valuemax')).toBe('200');
});
it('adjusts the "bar" width when it changes', function() {
expect(getBar(0).css('width')).toBe('5%');
$rootScope.max = 250;
$rootScope.$digest();
expect(getBar(0).css('width')).toBe('4%');
});
it('adjusts the "bar" width when value changes', function() {
$rootScope.objects[0].value = 60;
$rootScope.$digest();
expect(getBar(0).css('width')).toBe('30%');
$rootScope.objects[0].value += 12;
$rootScope.$digest();
expect(getBar(0).css('width')).toBe('36%');
$rootScope.objects[0].value = 0;
$rootScope.$digest();
expect(getBar(0).css('width')).toBe('0%');
});
it('transcludes "bar" text', function() {
expect(getBar(0).text()).toBe('10/200');
});
it('adjusts the valuemax when it changes', function() {
expect(getBar(0).attr('aria-valuemax')).toBe('200');
$rootScope.max = 300;
$rootScope.$digest();
expect(getBar(0).attr('aria-valuemax')).toBe('300');
});
it('should not have a total width over 100%', function() {
$rootScope.objects = [
{ value: 60, type: 'warning' },
{ value: 103 },
{ value: 270, type: 'info' }
];
$rootScope.max = 433;
$rootScope.$digest();
var totalWidth = 0;
for (var i = 0; i < 3; i++) {
totalWidth += parseFloat(getBar(i).css('width'));
}
expect(totalWidth.toFixed(2)).toBe('100.00');
});
it('should not have a total width over 37.65% when removing bar', function() {
$rootScope.objects = [
{ value: 60, type: 'warning' },
{ value: 103 },
{ value: 270, type: 'info' }
];
$rootScope.max = 433;
$rootScope.$digest();
var totalWidth = 0;
var i;
for (i = 0; i < 3; i++) {
totalWidth += parseFloat(getBar(i).css('width'));
}
expect(totalWidth.toFixed(2)).toBe('100.00');
$rootScope.objects.splice(2, 1);
$rootScope.$digest();
totalWidth = 0;
for (i = 0; i < 2; i++) {
totalWidth += parseFloat(getBar(i).css('width'));
}
expect(totalWidth.toFixed(2)).toBe('37.65');
});
});
});
});
================================================
FILE: src/rating/docs/demo.html
================================================
Default
{{percent}}%
Rate: {{rate}} - Readonly is: {{isReadonly}} - Hovering over: {{overStar || "none"}}
Custom icons
(Rate: {{x}})
(Rate: {{y}})
================================================
FILE: src/rating/docs/demo.js
================================================
angular.module('ui.bootstrap.demo').controller('RatingDemoCtrl', function ($scope) {
$scope.rate = 7;
$scope.max = 10;
$scope.isReadonly = false;
$scope.hoveringOver = function(value) {
$scope.overStar = value;
$scope.percent = 100 * (value / $scope.max);
};
$scope.ratingStates = [
{stateOn: 'glyphicon-ok-sign', stateOff: 'glyphicon-ok-circle'},
{stateOn: 'glyphicon-star', stateOff: 'glyphicon-star-empty'},
{stateOn: 'glyphicon-heart', stateOff: 'glyphicon-ban-circle'},
{stateOn: 'glyphicon-heart'},
{stateOff: 'glyphicon-off'}
];
});
================================================
FILE: src/rating/docs/readme.md
================================================
Rating directive that will take care of visualising a star rating bar.
### uib-rating settings
* `max`
$
C
_(Default: `5`)_ -
Changes the number of icons.
* `ng-model`
$
-
The current rate.
* `on-hover(value)`
$ -
An optional expression called when user's mouse is over a particular icon.
* `on-leave()`
$ -
An optional expression called when user's mouse leaves the control altogether.
* `rating-states`
$
_(Default: `null`)_ -
An array of objects defining properties for all icons. In default template, `stateOn` & `stateOff` property is used to specify the icon's class.
* `read-only`
$
_(Default: `false`)_ -
Prevent user's interaction.
* `titles`
$
C
_(Default: ['one', 'two', 'three', 'four', 'five']`)_ -
An array of strings defining titles for all icons.
* `enable-reset`
$
_(Default: `true`)_ -
Clicking the icon of the current rating will reset the rating to 0.
* `state-off`
$
C
_(Default: `null`)_ -
A variable used in the template to specify the state for unselected icons.
* `state-on`
$
C
_(Default: `null`)_ -
A variable used in the template to specify the state (class, src, etc) for selected icons.
================================================
FILE: src/rating/index.js
================================================
require('../../template/rating/rating.html.js');
require('./rating');
var MODULE_NAME = 'ui.bootstrap.module.rating';
angular.module(MODULE_NAME, ['ui.bootstrap.rating', 'uib/template/rating/rating.html']);
module.exports = MODULE_NAME;
================================================
FILE: src/rating/rating.js
================================================
angular.module('ui.bootstrap.rating', [])
.constant('uibRatingConfig', {
max: 5,
stateOn: null,
stateOff: null,
enableReset: true,
titles: ['one', 'two', 'three', 'four', 'five']
})
.controller('UibRatingController', ['$scope', '$attrs', 'uibRatingConfig', function($scope, $attrs, ratingConfig) {
var ngModelCtrl = { $setViewValue: angular.noop },
self = this;
this.init = function(ngModelCtrl_) {
ngModelCtrl = ngModelCtrl_;
ngModelCtrl.$render = this.render;
ngModelCtrl.$formatters.push(function(value) {
if (angular.isNumber(value) && value << 0 !== value) {
value = Math.round(value);
}
return value;
});
this.stateOn = angular.isDefined($attrs.stateOn) ? $scope.$parent.$eval($attrs.stateOn) : ratingConfig.stateOn;
this.stateOff = angular.isDefined($attrs.stateOff) ? $scope.$parent.$eval($attrs.stateOff) : ratingConfig.stateOff;
this.enableReset = angular.isDefined($attrs.enableReset) ?
$scope.$parent.$eval($attrs.enableReset) : ratingConfig.enableReset;
var tmpTitles = angular.isDefined($attrs.titles) ? $scope.$parent.$eval($attrs.titles) : ratingConfig.titles;
this.titles = angular.isArray(tmpTitles) && tmpTitles.length > 0 ?
tmpTitles : ratingConfig.titles;
var ratingStates = angular.isDefined($attrs.ratingStates) ?
$scope.$parent.$eval($attrs.ratingStates) :
new Array(angular.isDefined($attrs.max) ? $scope.$parent.$eval($attrs.max) : ratingConfig.max);
$scope.range = this.buildTemplateObjects(ratingStates);
};
this.buildTemplateObjects = function(states) {
for (var i = 0, n = states.length; i < n; i++) {
states[i] = angular.extend({ index: i }, { stateOn: this.stateOn, stateOff: this.stateOff, title: this.getTitle(i) }, states[i]);
}
return states;
};
this.getTitle = function(index) {
if (index >= this.titles.length) {
return index + 1;
}
return this.titles[index];
};
$scope.rate = function(value) {
if (!$scope.readonly && value >= 0 && value <= $scope.range.length) {
var newViewValue = self.enableReset && ngModelCtrl.$viewValue === value ? 0 : value;
ngModelCtrl.$setViewValue(newViewValue);
ngModelCtrl.$render();
}
};
$scope.enter = function(value) {
if (!$scope.readonly) {
$scope.value = value;
}
$scope.onHover({value: value});
};
$scope.reset = function() {
$scope.value = ngModelCtrl.$viewValue;
$scope.onLeave();
};
$scope.onKeydown = function(evt) {
if (/(37|38|39|40)/.test(evt.which)) {
evt.preventDefault();
evt.stopPropagation();
$scope.rate($scope.value + (evt.which === 38 || evt.which === 39 ? 1 : -1));
}
};
this.render = function() {
$scope.value = ngModelCtrl.$viewValue;
$scope.title = self.getTitle($scope.value - 1);
};
}])
.directive('uibRating', function() {
return {
require: ['uibRating', 'ngModel'],
restrict: 'A',
scope: {
readonly: '=?readOnly',
onHover: '&',
onLeave: '&'
},
controller: 'UibRatingController',
templateUrl: 'uib/template/rating/rating.html',
link: function(scope, element, attrs, ctrls) {
var ratingCtrl = ctrls[0], ngModelCtrl = ctrls[1];
ratingCtrl.init(ngModelCtrl);
}
};
});
================================================
FILE: src/rating/test/rating.spec.js
================================================
describe('rating directive', function() {
var $rootScope, $compile, element, innerElem;
beforeEach(module('ui.bootstrap.rating'));
beforeEach(module('uib/template/rating/rating.html'));
beforeEach(inject(function(_$compile_, _$rootScope_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
$rootScope.rate = 3;
element = $compile('')($rootScope);
$rootScope.$digest();
innerElem = element.children().eq(0);
}));
function getStars() {
return innerElem.find('i');
}
function getStar(number) {
return getStars().eq(number - 1);
}
function getState(classOn, classOff) {
var stars = getStars();
var state = [];
for (var i = 0, n = stars.length; i < n; i++) {
state.push(stars.eq(i).hasClass(classOn || 'glyphicon-star') &&
!stars.eq(i).hasClass(classOff || 'glyphicon-star-empty'));
}
return state;
}
function getTitles() {
var stars = getStars();
return stars.toArray().map(function(star) {
return angular.element(star).attr('title');
});
}
function triggerKeyDown(keyCode) {
var e = $.Event('keydown');
e.which = keyCode;
innerElem.trigger(e);
}
it('contains the default number of icons', function() {
expect(getStars().length).toBe(5);
expect(innerElem.attr('aria-valuemax')).toBe('5');
});
it('initializes the default star icons as selected', function() {
expect(getState()).toEqual([true, true, true, false, false]);
expect(innerElem.attr('aria-valuenow')).toBe('3');
});
it('handles correctly the click event', function() {
getStar(2).click();
$rootScope.$digest();
expect(getState()).toEqual([true, true, false, false, false]);
expect($rootScope.rate).toBe(2);
expect(innerElem.attr('aria-valuenow')).toBe('2');
getStar(5).click();
$rootScope.$digest();
expect(getState()).toEqual([true, true, true, true, true]);
expect($rootScope.rate).toBe(5);
expect(innerElem.attr('aria-valuenow')).toBe('5');
getStar(5).click();
$rootScope.$digest();
expect(getState()).toEqual([false, false, false, false, false]);
expect($rootScope.rate).toBe(0);
expect(innerElem.attr('aria-valuenow')).toBe('0');
});
it('handles correctly the hover event', function() {
getStar(2).trigger('mouseover');
$rootScope.$digest();
expect(getState()).toEqual([true, true, false, false, false]);
expect($rootScope.rate).toBe(3);
getStar(5).trigger('mouseover');
$rootScope.$digest();
expect(getState()).toEqual([true, true, true, true, true]);
expect($rootScope.rate).toBe(3);
innerElem.trigger('mouseout');
expect(getState()).toEqual([true, true, true, false, false]);
expect($rootScope.rate).toBe(3);
});
it('rounds off the number of stars shown with decimal values', function() {
$rootScope.rate = 2.1;
$rootScope.$digest();
expect(getState()).toEqual([true, true, false, false, false]);
expect(innerElem.attr('aria-valuenow')).toBe('2');
$rootScope.rate = 2.5;
$rootScope.$digest();
expect(getState()).toEqual([true, true, true, false, false]);
expect(innerElem.attr('aria-valuenow')).toBe('3');
});
it('changes the number of selected icons when value changes', function() {
$rootScope.rate = 2;
$rootScope.$digest();
expect(getState()).toEqual([true, true, false, false, false]);
expect(innerElem.attr('aria-valuenow')).toBe('2');
expect(innerElem.attr('aria-valuetext')).toBe('two');
});
it('shows different number of icons when `max` attribute is set', function() {
element = $compile('')($rootScope);
$rootScope.$digest();
innerElem = element.children().eq(0);
expect(getStars().length).toBe(7);
expect(innerElem.attr('aria-valuemax')).toBe('7');
});
it('shows different number of icons when `max` attribute is from scope variable', function() {
$rootScope.max = 15;
element = $compile('')($rootScope);
$rootScope.$digest();
innerElem = element.children().eq(0);
expect(getStars().length).toBe(15);
expect(innerElem.attr('aria-valuemax')).toBe('15');
});
it('handles read-only attribute', function() {
$rootScope.isReadonly = true;
element = $compile('')($rootScope);
$rootScope.$digest();
innerElem = element.children().eq(0);
expect(getState()).toEqual([true, true, true, false, false]);
var star5 = getStar(5);
star5.trigger('mouseover');
$rootScope.$digest();
expect(getState()).toEqual([true, true, true, false, false]);
$rootScope.isReadonly = false;
$rootScope.$digest();
star5.trigger('mouseover');
$rootScope.$digest();
expect(getState()).toEqual([true, true, true, true, true]);
});
it('handles enable-reset attribute', function() {
$rootScope.canReset = false;
element = $compile('')($rootScope);
$rootScope.$digest();
innerElem = element.children().eq(0);
var star = {
states: [true, true, true, true, true],
rating: 5
};
var selectStar = getStar(star.rating);
selectStar.click();
$rootScope.$digest();
expect(getState()).toEqual(star.states);
expect($rootScope.rate).toBe(5);
expect(innerElem.attr('aria-valuenow')).toBe('5');
selectStar.click();
$rootScope.$digest();
expect(getState()).toEqual(star.states);
expect($rootScope.rate).toBe(5);
expect(innerElem.attr('aria-valuenow')).toBe('5');
});
it('should fire onHover', function() {
$rootScope.hoveringOver = jasmine.createSpy('hoveringOver');
element = $compile('')($rootScope);
$rootScope.$digest();
innerElem = element.children().eq(0);
getStar(3).trigger('mouseover');
$rootScope.$digest();
expect($rootScope.hoveringOver).toHaveBeenCalledWith(3);
});
it('should fire onLeave', function() {
$rootScope.leaving = jasmine.createSpy('leaving');
element = $compile('')($rootScope);
$rootScope.$digest();
innerElem = element.children().eq(0);
innerElem.trigger('mouseleave');
$rootScope.$digest();
expect($rootScope.leaving).toHaveBeenCalled();
});
describe('keyboard navigation', function() {
it('supports arrow keys', function() {
triggerKeyDown(38);
expect($rootScope.rate).toBe(4);
triggerKeyDown(37);
expect($rootScope.rate).toBe(3);
triggerKeyDown(40);
expect($rootScope.rate).toBe(2);
triggerKeyDown(39);
expect($rootScope.rate).toBe(3);
});
it('supports only arrow keys', function() {
$rootScope.rate = undefined;
$rootScope.$digest();
triggerKeyDown(36);
expect($rootScope.rate).toBe(undefined);
triggerKeyDown(41);
expect($rootScope.rate).toBe(undefined);
});
it('can get zero value but not negative', function() {
$rootScope.rate = 1;
$rootScope.$digest();
triggerKeyDown(37);
expect($rootScope.rate).toBe(0);
triggerKeyDown(37);
expect($rootScope.rate).toBe(0);
});
it('cannot get value above max', function() {
$rootScope.rate = 4;
$rootScope.$digest();
triggerKeyDown(38);
expect($rootScope.rate).toBe(5);
triggerKeyDown(38);
expect($rootScope.rate).toBe(5);
});
});
describe('custom states', function() {
beforeEach(inject(function() {
$rootScope.classOn = 'icon-ok-sign';
$rootScope.classOff = 'icon-ok-circle';
element = $compile('')($rootScope);
$rootScope.$digest();
innerElem = element.children().eq(0);
}));
it('changes the default icons', function() {
expect(getState($rootScope.classOn, $rootScope.classOff)).toEqual([true, true, true, false, false]);
});
});
describe('`rating-states`', function() {
beforeEach(inject(function() {
$rootScope.states = [
{stateOn: 'sign', stateOff: 'circle'},
{stateOn: 'heart', stateOff: 'ban'},
{stateOn: 'heart'},
{stateOff: 'off'}
];
element = $compile('')($rootScope);
$rootScope.$digest();
innerElem = element.children().eq(0);
}));
it('should define number of icon elements', function() {
expect(getStars().length).toBe(4);
expect(innerElem.attr('aria-valuemax')).toBe('4');
});
it('handles each icon', function() {
var stars = getStars();
for (var i = 0; i < stars.length; i++) {
var star = stars.eq(i);
var state = $rootScope.states[i];
var isOn = i < $rootScope.rate;
expect(star.hasClass(state.stateOn)).toBe(isOn);
expect(star.hasClass(state.stateOff)).toBe(!isOn);
}
});
});
describe('setting uibRatingConfig', function() {
var originalConfig = {};
beforeEach(inject(function(uibRatingConfig) {
$rootScope.rate = 5;
angular.extend(originalConfig, uibRatingConfig);
uibRatingConfig.max = 10;
uibRatingConfig.stateOn = 'on';
uibRatingConfig.stateOff = 'off';
element = $compile('')($rootScope);
$rootScope.$digest();
innerElem = element.children().eq(0);
}));
afterEach(inject(function(uibRatingConfig) {
// return it to the original state
angular.extend(uibRatingConfig, originalConfig);
}));
it('should change number of icon elements', function() {
expect(getStars().length).toBe(10);
});
it('should change icon states', function() {
expect(getState('on', 'off')).toEqual([true, true, true, true, true, false, false, false, false, false]);
});
});
describe('Default title', function() {
it('should return the default title for each star', function() {
expect(getTitles()).toEqual(['one', 'two', 'three', 'four', 'five']);
});
});
describe('shows different title when `max` attribute is greater than the titles array ', function() {
var originalConfig = {};
beforeEach(inject(function(uibRatingConfig) {
$rootScope.rate = 5;
angular.extend(originalConfig, uibRatingConfig);
uibRatingConfig.max = 10;
element = $compile('')($rootScope);
$rootScope.$digest();
innerElem = element.children().eq(0);
}));
afterEach(inject(function(uibRatingConfig) {
// return it to the original state
angular.extend(uibRatingConfig, originalConfig);
}));
it('should return the default title for each star', function() {
expect(getTitles()).toEqual(['one', 'two', 'three', 'four', 'five', '6', '7', '8', '9', '10']);
});
});
describe('shows custom titles ', function() {
it('should return the custom title for each star', function() {
$rootScope.titles = [44,45,46];
element = $compile('')($rootScope);
$rootScope.$digest();
innerElem = element.children().eq(0);
expect(getTitles()).toEqual(['44', '45', '46', '4', '5']);
});
it('should return the default title if the custom title is empty', function() {
$rootScope.titles = [];
element = $compile('')($rootScope);
$rootScope.$digest();
innerElem = element.children().eq(0);
expect(getTitles()).toEqual(['one', 'two', 'three', 'four', 'five']);
});
it('should return the default title if the custom title is not an array', function() {
element = $compile('')($rootScope);
$rootScope.$digest();
expect(getTitles()).toEqual(['one', 'two', 'three', 'four', 'five']);
});
});
});
================================================
FILE: src/stackedMap/index.js
================================================
require('./stackedMap');
var MODULE_NAME = 'ui.bootstrap.module.stackedMap';
angular.module(MODULE_NAME, ['ui.bootstrap.stackedMap']);
module.exports = MODULE_NAME;
================================================
FILE: src/stackedMap/stackedMap.js
================================================
angular.module('ui.bootstrap.stackedMap', [])
/**
* A helper, internal data structure that acts as a map but also allows getting / removing
* elements in the LIFO order
*/
.factory('$$stackedMap', function() {
return {
createNew: function() {
var stack = [];
return {
add: function(key, value) {
stack.push({
key: key,
value: value
});
},
get: function(key) {
for (var i = 0; i < stack.length; i++) {
if (key === stack[i].key) {
return stack[i];
}
}
},
keys: function() {
var keys = [];
for (var i = 0; i < stack.length; i++) {
keys.push(stack[i].key);
}
return keys;
},
top: function() {
return stack[stack.length - 1];
},
remove: function(key) {
var idx = -1;
for (var i = 0; i < stack.length; i++) {
if (key === stack[i].key) {
idx = i;
break;
}
}
return stack.splice(idx, 1)[0];
},
removeTop: function() {
return stack.pop();
},
length: function() {
return stack.length;
}
};
}
};
});
================================================
FILE: src/stackedMap/test/stackedMap.spec.js
================================================
describe('stacked map', function() {
var stackedMap;
beforeEach(module('ui.bootstrap.modal'));
beforeEach(inject(function ($$stackedMap) {
stackedMap = $$stackedMap.createNew();
}));
it('should add and remove objects by key', function() {
stackedMap.add('foo', 'foo_value');
expect(stackedMap.length()).toEqual(1);
expect(stackedMap.get('foo').key).toEqual('foo');
expect(stackedMap.get('foo').value).toEqual('foo_value');
stackedMap.remove('foo');
expect(stackedMap.length()).toEqual(0);
expect(stackedMap.get('foo')).toBeUndefined();
});
it('should support listing keys', function() {
stackedMap.add('foo', 'foo_value');
stackedMap.add('bar', 'bar_value');
expect(stackedMap.keys()).toEqual(['foo', 'bar']);
});
it('should get topmost element', function() {
stackedMap.add('foo', 'foo_value');
stackedMap.add('bar', 'bar_value');
expect(stackedMap.length()).toEqual(2);
expect(stackedMap.top().key).toEqual('bar');
expect(stackedMap.length()).toEqual(2);
});
it('should remove topmost element', function() {
stackedMap.add('foo', 'foo_value');
stackedMap.add('bar', 'bar_value');
expect(stackedMap.removeTop().key).toEqual('bar');
expect(stackedMap.removeTop().key).toEqual('foo');
});
it('should preserve semantic of an empty stackedMap', function() {
expect(stackedMap.length()).toEqual(0);
expect(stackedMap.top()).toBeUndefined();
});
it('should ignore removal of non-existing elements', function() {
expect(stackedMap.remove('non-existing')).toBeUndefined();
});
});
================================================
FILE: src/tabindex/index.js
================================================
require('./tabindex');
var MODULE_NAME = 'ui.bootstrap.module.tabindex';
angular.module(MODULE_NAME, ['ui.bootstrap.tabindex']);
module.exports = MODULE_NAME;
================================================
FILE: src/tabindex/tabindex.js
================================================
angular.module('ui.bootstrap.tabindex', [])
.directive('uibTabindexToggle', function() {
return {
restrict: 'A',
link: function(scope, elem, attrs) {
attrs.$observe('disabled', function(disabled) {
attrs.$set('tabindex', disabled ? -1 : null);
});
}
};
});
================================================
FILE: src/tabindex/test/tabindex.spec.js
================================================
describe('tabindex toggle directive', function() {
var $rootScope, element;
beforeEach(module('ui.bootstrap.tabindex'));
beforeEach(inject(function($compile, _$rootScope_) {
$rootScope = _$rootScope_;
element = $compile('foo')($rootScope);
$rootScope.$digest();
}));
it('should toggle the tabindex on disabled toggle', function() {
expect(element.prop('tabindex')).toBe(0);
$rootScope.disabled = true;
$rootScope.$digest();
expect(element.prop('tabindex')).toBe(-1);
$rootScope.disabled = false;
$rootScope.$digest();
expect(element.prop('tabindex')).toBe(0);
});
});
================================================
FILE: src/tabs/docs/demo.html
================================================
Select a tab by setting active binding to true:
Static content
{{tab.content}}
Alert!
I've got an HTML heading, and a select callback. Pretty cool!
Vertical content 1
Vertical content 2
Justified content
Short Labeled Justified content
Long Labeled Justified content
Tabbed pills with CSS classes
Tab 1 content
Tab 2 content
Tabs using nested forms:
Model:
{{ model | json }}
Nested Form:
{{ outerForm.nestedForm | json }}
================================================
FILE: src/tabs/docs/demo.js
================================================
angular.module('ui.bootstrap.demo').controller('TabsDemoCtrl', function ($scope, $window) {
$scope.tabs = [
{ title:'Dynamic Title 1', content:'Dynamic content 1' },
{ title:'Dynamic Title 2', content:'Dynamic content 2', disabled: true }
];
$scope.alertMe = function() {
setTimeout(function() {
$window.alert('You\'ve selected the alert tab!');
});
};
$scope.model = {
name: 'Tabs'
};
});
================================================
FILE: src/tabs/docs/readme.md
================================================
AngularJS version of the tabs directive.
### uib-tabset settings
* `active`
_(Default: `Index of first tab`)_ -
Active index of tab. Setting this to an existing tab index will make that tab active.
* `justified`
$
_(Default: `false`)_ -
Whether tabs fill the container and have a consistent width.
* `template-url`
_(Default: `uib/template/tabs/tabset.html`)_ -
A URL representing the location of a template to use for the main component.
* `type`
_(Defaults: `tabs`)_ -
Navigation type. Possible values are 'tabs' and 'pills'.
* `vertical`
$
_(Default: `false`)_ -
Whether tabs appear vertically stacked.
### uib-tab settings
* `classes`
$ -
An optional string of space-separated CSS classes.
* `deselect()`
$ -
An optional expression called when tab is deactivated. Supports `$event` and `$selectedIndex` in template for expression. You may call `$event.preventDefault()` in this event handler to prevent a tab change from occurring. The `$selectedIndex` can be used to determine which tab was attempted to be opened.
* `disable`
$
_(Default: `false`)_ -
Whether tab is clickable and can be activated.
* `heading` -
Heading text.
* `index` -
Tab index. Must be unique number or string.
* `select()`
$ -
An optional expression called when tab is activated. Supports $event in template for expression.
* `template-url`
_(Default: `uib/template/tabs/tab.html`)_ -
A URL representing the location of a template to use for the tab heading.
### Tabset heading
Instead of the `heading` attribute on the `uib-tabset`, you can use an `uib-tab-heading` element inside a tabset that will be used as the tabset's header. There you can use HTML as well.
### Known issues
To use clickable elements within the tab, you have override the tab template to use div elements instead of anchor elements, and replicate the desired styles from Bootstrap's CSS. This is due to browsers interpreting anchor elements as the target of any click event, which triggers routing when certain elements such as buttons are nested inside the anchor element.
================================================
FILE: src/tabs/index.js
================================================
require('../../template/tabs/tab.html.js');
require('../../template/tabs/tabset.html.js');
require('./tabs');
var MODULE_NAME = 'ui.bootstrap.module.tabs';
angular.module(MODULE_NAME, ['ui.bootstrap.tabs', 'uib/template/tabs/tab.html', 'uib/template/tabs/tabset.html']);
module.exports = MODULE_NAME;
================================================
FILE: src/tabs/tabs.js
================================================
angular.module('ui.bootstrap.tabs', [])
.controller('UibTabsetController', ['$scope', function ($scope) {
var ctrl = this,
oldIndex;
ctrl.tabs = [];
ctrl.select = function(index, evt) {
if (!destroyed) {
var previousIndex = findTabIndex(oldIndex);
var previousSelected = ctrl.tabs[previousIndex];
if (previousSelected) {
previousSelected.tab.onDeselect({
$event: evt,
$selectedIndex: index
});
if (evt && evt.isDefaultPrevented()) {
return;
}
previousSelected.tab.active = false;
}
var selected = ctrl.tabs[index];
if (selected) {
selected.tab.onSelect({
$event: evt
});
selected.tab.active = true;
ctrl.active = selected.index;
oldIndex = selected.index;
} else if (!selected && angular.isDefined(oldIndex)) {
ctrl.active = null;
oldIndex = null;
}
}
};
ctrl.addTab = function addTab(tab) {
ctrl.tabs.push({
tab: tab,
index: tab.index
});
ctrl.tabs.sort(function(t1, t2) {
if (t1.index > t2.index) {
return 1;
}
if (t1.index < t2.index) {
return -1;
}
return 0;
});
if (tab.index === ctrl.active || !angular.isDefined(ctrl.active) && ctrl.tabs.length === 1) {
var newActiveIndex = findTabIndex(tab.index);
ctrl.select(newActiveIndex);
}
};
ctrl.removeTab = function removeTab(tab) {
var index;
for (var i = 0; i < ctrl.tabs.length; i++) {
if (ctrl.tabs[i].tab === tab) {
index = i;
break;
}
}
if (ctrl.tabs[index].index === ctrl.active) {
var newActiveTabIndex = index === ctrl.tabs.length - 1 ?
index - 1 : index + 1 % ctrl.tabs.length;
ctrl.select(newActiveTabIndex);
}
ctrl.tabs.splice(index, 1);
};
$scope.$watch('tabset.active', function(val) {
if (angular.isDefined(val) && val !== oldIndex) {
ctrl.select(findTabIndex(val));
}
});
var destroyed;
$scope.$on('$destroy', function() {
destroyed = true;
});
function findTabIndex(index) {
for (var i = 0; i < ctrl.tabs.length; i++) {
if (ctrl.tabs[i].index === index) {
return i;
}
}
}
}])
.directive('uibTabset', function() {
return {
transclude: true,
replace: true,
scope: {},
bindToController: {
active: '=?',
type: '@'
},
controller: 'UibTabsetController',
controllerAs: 'tabset',
templateUrl: function(element, attrs) {
return attrs.templateUrl || 'uib/template/tabs/tabset.html';
},
link: function(scope, element, attrs) {
scope.vertical = angular.isDefined(attrs.vertical) ?
scope.$parent.$eval(attrs.vertical) : false;
scope.justified = angular.isDefined(attrs.justified) ?
scope.$parent.$eval(attrs.justified) : false;
}
};
})
.directive('uibTab', ['$parse', function($parse) {
return {
require: '^uibTabset',
replace: true,
templateUrl: function(element, attrs) {
return attrs.templateUrl || 'uib/template/tabs/tab.html';
},
transclude: true,
scope: {
heading: '@',
index: '=?',
classes: '@?',
onSelect: '&select', //This callback is called in contentHeadingTransclude
//once it inserts the tab's content into the dom
onDeselect: '&deselect'
},
controller: function() {
//Empty controller so other directives can require being 'under' a tab
},
controllerAs: 'tab',
link: function(scope, elm, attrs, tabsetCtrl, transclude) {
scope.disabled = false;
if (attrs.disable) {
scope.$parent.$watch($parse(attrs.disable), function(value) {
scope.disabled = !! value;
});
}
if (angular.isUndefined(attrs.index)) {
if (tabsetCtrl.tabs && tabsetCtrl.tabs.length) {
scope.index = Math.max.apply(null, tabsetCtrl.tabs.map(function(t) { return t.index; })) + 1;
} else {
scope.index = 0;
}
}
if (angular.isUndefined(attrs.classes)) {
scope.classes = '';
}
scope.select = function(evt) {
if (!scope.disabled) {
var index;
for (var i = 0; i < tabsetCtrl.tabs.length; i++) {
if (tabsetCtrl.tabs[i].tab === scope) {
index = i;
break;
}
}
tabsetCtrl.select(index, evt);
}
};
tabsetCtrl.addTab(scope);
scope.$on('$destroy', function() {
tabsetCtrl.removeTab(scope);
});
//We need to transclude later, once the content container is ready.
//when this link happens, we're inside a tab heading.
scope.$transcludeFn = transclude;
}
};
}])
.directive('uibTabHeadingTransclude', function() {
return {
restrict: 'A',
require: '^uibTab',
link: function(scope, elm) {
scope.$watch('headingElement', function updateHeadingElement(heading) {
if (heading) {
elm.html('');
elm.append(heading);
}
});
}
};
})
.directive('uibTabContentTransclude', function() {
return {
restrict: 'A',
require: '^uibTabset',
link: function(scope, elm, attrs) {
var tab = scope.$eval(attrs.uibTabContentTransclude).tab;
//Now our tab is ready to be transcluded: both the tab heading area
//and the tab content area are loaded. Transclude 'em both.
tab.$transcludeFn(tab.$parent, function(contents) {
angular.forEach(contents, function(node) {
if (isTabHeading(node)) {
//Let tabHeadingTransclude know.
tab.headingElement = node;
} else {
elm.append(node);
}
});
});
}
};
function isTabHeading(node) {
return node.tagName && (
node.hasAttribute('uib-tab-heading') ||
node.hasAttribute('data-uib-tab-heading') ||
node.hasAttribute('x-uib-tab-heading') ||
node.tagName.toLowerCase() === 'uib-tab-heading' ||
node.tagName.toLowerCase() === 'data-uib-tab-heading' ||
node.tagName.toLowerCase() === 'x-uib-tab-heading' ||
node.tagName.toLowerCase() === 'uib:tab-heading'
);
}
});
================================================
FILE: src/tabs/test/tabs.spec.js
================================================
describe('tabs', function() {
var elm, scope;
beforeEach(module('ui.bootstrap.tabs'));
beforeEach(module('uib/template/tabs/tabset.html'));
beforeEach(module('uib/template/tabs/tab.html'));
function titles() {
return elm.find('ul.nav-tabs li');
}
function contents() {
return elm.find('div.tab-content div.tab-pane');
}
function expectTitles(titlesArray) {
var t = titles();
expect(t.length).toEqual(titlesArray.length);
for (var i = 0; i < t.length; i++) {
expect(t.eq(i).text().trim()).toEqual(titlesArray[i]);
}
}
function expectContents(contentsArray) {
var c = contents();
expect(c.length).toEqual(contentsArray.length);
for (var i = 0; i < c.length; i++) {
expect(c.eq(i).text().trim()).toEqual(contentsArray[i]);
}
}
describe('basics', function() {
beforeEach(inject(function($compile, $rootScope) {
scope = $rootScope.$new();
scope.first = '1';
scope.second = '2';
scope.third = '3';
scope.active = 1;
scope.firstClass = 'first-class';
scope.secondClass = 'second-class-1 second-class-2';
scope.selectFirst = jasmine.createSpy();
scope.selectSecond = jasmine.createSpy();
scope.deselectFirst = jasmine.createSpy();
scope.deselectSecond = jasmine.createSpy();
scope.deselectThird = function($event) {
$event.preventDefault();
};
elm = $compile([
'',
' ',
' first content is {{first}}',
' ',
' ',
' Second Tab {{second}}',
' second content is {{second}}',
' ',
' ',
' Second Tab {{third}}',
' third content is {{third}}',
' ',
''
].join('\n'))(scope);
scope.$apply();
return elm;
}));
it('should pass class and other attributes on to tab template', function() {
expect(elm).toHaveClass('hello');
expect(elm.attr('data-pizza')).toBe('pepperoni');
//Ensure that we have bootstrap 4 link class so things are future proofed.
var link = $(elm.find('a')[0]);
expect(link).toHaveClass('nav-link');
});
it('should create clickable titles', function() {
var t = titles();
expect(t.length).toBe(3);
expect(t.find('> a').eq(0).text()).toBe('First Tab 1');
//It should put the uib-tab-heading element into the 'a' title
expect(t.find('> a').eq(1).children().is('uib-tab-heading')).toBe(true);
expect(t.find('> a').eq(1).children().html()).toBe('Second Tab 2');
});
it('should bind tabs content and set first tab active', function() {
expectContents(['first content is 1', 'second content is 2', 'third content is 3']);
expect(titles().eq(0)).toHaveClass('active');
expect(titles().eq(1)).not.toHaveClass('active');
expect(scope.active).toBe(1);
});
it('should set optional classes on each tab', function() {
expect(titles().eq(0)).toHaveClass(scope.firstClass);
var secondClassArr = scope.secondClass.split(' ');
secondClassArr.forEach(function(clazz) {
expect(titles().eq(1)).toHaveClass(clazz);
});
});
it('should change active on click', function() {
titles().eq(1).find('> a').click();
expect(contents().eq(1)).toHaveClass('active');
expect(titles().eq(0)).not.toHaveClass('active');
expect(titles().eq(1)).toHaveClass('active');
expect(scope.active).toBe(2);
});
it('should call select callback on select', function() {
expect(scope.selectFirst.calls.count()).toBe(1);
titles().eq(1).find('> a').click();
expect(scope.selectSecond).toHaveBeenCalled();
expect(scope.selectSecond.calls.argsFor(0)[0].target).toBe(titles().eq(1).find('> a')[0]);
titles().eq(0).find('> a').click();
expect(scope.selectFirst).toHaveBeenCalled();
expect(scope.selectFirst.calls.argsFor(1)[0].target).toBe(titles().eq(0).find('> a')[0]);
});
it('should call deselect callback on deselect', function() {
titles().eq(1).find('> a').click();
expect(scope.deselectFirst).toHaveBeenCalled();
expect(scope.deselectFirst.calls.argsFor(0)[0].target).toBe(titles().eq(1).find('> a')[0]);
expect(scope.deselectFirst.calls.argsFor(0)[1]).toBe(1);
titles().eq(0).find('> a').click();
expect(scope.deselectSecond).toHaveBeenCalled();
expect(scope.deselectSecond.calls.argsFor(0)[0].target).toBe(titles().eq(0).find('> a')[0]);
expect(scope.deselectSecond.calls.argsFor(0)[1]).toBe(0);
titles().eq(1).find('> a').click();
expect(scope.deselectFirst.calls.count()).toBe(2);
expect(scope.deselectFirst.calls.argsFor(1)[0].target).toBe(titles().eq(1).find('> a')[0]);
expect(scope.deselectFirst.calls.argsFor(1)[1]).toBe(1);
});
it('should prevent tab deselection when $event.preventDefault() is called', function() {
spyOn(scope, 'deselectThird');
titles().eq(2).find('> a').click();
expect(scope.active).toBe(3);
titles().eq(1).find('> a').click();
expect(scope.deselectThird).toHaveBeenCalled();
expect(scope.active).not.toBe(1);
expect(scope.active).toBe(2);
});
});
describe('basics with initial active tab', function() {
beforeEach(inject(function($compile, $rootScope) {
scope = $rootScope.$new();
function makeTab(index) {
return {
index: index,
select: jasmine.createSpy()
};
}
scope.tabs = [
makeTab(1), makeTab(3), makeTab(5), makeTab(7)
];
scope.active = 5;
elm = $compile([
'',
' ',
' ',
' ',
' ',
' ',
' ',
' ',
' ',
''
].join('\n'))(scope);
scope.$apply();
}));
function expectTabActive(activeTab) {
var _titles = titles();
angular.forEach(scope.tabs, function(tab, i) {
if (activeTab === tab) {
expect(scope.active).toBe(tab.index);
//It should only call select ONCE for each select
expect(tab.select).toHaveBeenCalled();
expect(_titles.eq(i)).toHaveClass('active');
expect(contents().eq(i)).toHaveClass('active');
} else {
expect(scope.active).not.toBe(tab.index);
expect(_titles.eq(i)).not.toHaveClass('active');
}
});
}
it('should make tab titles and set active tab active', function() {
expect(titles().length).toBe(scope.tabs.length);
expectTabActive(scope.tabs[2]);
});
});
describe('without active binding and index attributes', function() {
beforeEach(inject(function($compile, $rootScope) {
scope = $rootScope.$new();
scope.first = '1';
scope.second = '2';
elm = $compile([
'',
' ',
' first content is {{first}}',
' ',
' ',
' second content is {{second}}',
' ',
''
].join('\n'))(scope);
scope.$apply();
return elm;
}));
it('should bind tabs content and set first tab active', function() {
expectContents(['first content is 1', 'second content is 2']);
expect(titles().eq(0)).toHaveClass('active');
expect(titles().eq(1)).not.toHaveClass('active');
expect(elm.controller('uibTabset').active).toBe(0);
});
it('should change active on click', function() {
titles().eq(1).find('> a').click();
expect(contents().eq(1)).toHaveClass('active');
expect(titles().eq(0)).not.toHaveClass('active');
expect(titles().eq(1)).toHaveClass('active');
expect(elm.controller('uibTabset').active).toBe(1);
});
});
describe('index as strings', function() {
beforeEach(inject(function($compile, $rootScope) {
scope = $rootScope.$new();
scope.first = 'one';
scope.second = 'two';
scope.active = 'two';
elm = $compile([
'',
' ',
' first content',
' ',
' ',
' second content',
' ',
''
].join('\n'))(scope);
scope.$apply();
return elm;
}));
it('should set second tab active', function() {
expect(titles().eq(0)).not.toHaveClass('active');
expect(titles().eq(1)).toHaveClass('active');
expect(elm.controller('uibTabset').active).toBe('two');
});
it('should change active on click', function() {
expect(titles().eq(0)).not.toHaveClass('active');
titles().eq(0).find('> a').click();
expect(titles().eq(0)).toHaveClass('active');
expect(titles().eq(1)).not.toHaveClass('active');
expect(elm.controller('uibTabset').active).toBe('one');
});
});
describe('tab callback order', function() {
var execOrder;
beforeEach(inject(function($compile, $rootScope) {
scope = $rootScope.$new();
execOrder = [];
scope.execute = function(id) {
execOrder.push(id);
};
elm = $compile([
'',
' ',
' ',
' ',
' ',
'
'
].join('\n'))(scope);
scope.$apply();
return elm;
}));
it('should call select for the first tab', function() {
expect(execOrder).toEqual([ 'select1' ]);
});
it('should call deselect, then select', function() {
execOrder = [];
// Select second tab
titles().eq(1).find('> a').click();
expect(execOrder).toEqual([ 'deselect1', 'select2' ]);
execOrder = [];
// Select again first tab
titles().eq(0).find('> a').click();
expect(execOrder).toEqual([ 'deselect2', 'select1' ]);
});
});
describe('custom template', function() {
var $compile, $templateCache;
beforeEach(inject(function($rootScope, _$compile_, _$templateCache_) {
scope = $rootScope;
$compile = _$compile_;
$templateCache = _$templateCache_;
}));
it('should support custom templates', function() {
$templateCache.put('foo/bar.html', 'baz
');
elm = $compile('')(scope);
scope.$digest();
expect(elm.html()).toBe('baz');
});
});
describe('uib-tab', function() {
var $compile, $templateCache;
beforeEach(inject(function($rootScope, _$compile_, _$templateCache_) {
scope = $rootScope;
$compile = _$compile_;
$templateCache = _$templateCache_;
}));
it('should expose the controller on the view', function() {
$templateCache.put('uib/template/tabs/tab.html', '{{tab.text}}');
elm = $compile('')(scope);
scope.$digest();
var tab = titles().eq(0);
var ctrl = tab.controller('uibTab');
expect(ctrl).toBeDefined();
ctrl.text = 'foo';
scope.$digest();
expect(tab.text().trim()).toBe('foo');
});
it('should support custom templates', function() {
$templateCache.put('foo/bar.html', 'baz');
elm = $compile('')(scope);
scope.$digest();
var tabTitle = titles().eq(0);
expect(tabTitle.html()).toBe('baz');
});
});
describe('ng-repeat', function() {
var $compile, $rootScope;
beforeEach(inject(function(_$compile_, _$rootScope_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
scope = $rootScope.$new();
scope.tabs = [
makeTab(1), makeTab(3), makeTab(5), makeTab(7)
];
scope.active = 5;
elm = $compile([
'',
' ',
' heading {{index}}',
' content {{$index}}',
' ',
''
].join('\n'))(scope);
scope.$apply();
}));
function makeTab(index) {
return {
index: index,
select: jasmine.createSpy()
};
}
function titles() {
return elm.find('ul.nav-tabs li');
}
function contents() {
return elm.find('div.tab-content div.tab-pane');
}
function expectTabActive(activeTab) {
var _titles = titles();
angular.forEach(scope.tabs, function(tab, i) {
if (activeTab === tab) {
expect(scope.active).toBe(tab.index);
//It should only call select ONCE for each select
expect(tab.select).toHaveBeenCalled();
expect(_titles.eq(i)).toHaveClass('active');
expect(contents().eq(i).text().trim()).toBe('content ' + i);
expect(contents().eq(i)).toHaveClass('active');
} else {
expect(scope.active).not.toBe(tab.index);
expect(_titles.eq(i)).not.toHaveClass('active');
}
});
}
it('should make tab titles and set active tab active', function() {
expect(titles().length).toBe(scope.tabs.length);
expectTabActive(scope.tabs[2]);
});
it('should switch active when clicking', function() {
titles().eq(3).find('> a').click();
expectTabActive(scope.tabs[3]);
});
it('should switch active when changing active index', function() {
scope.$apply('active = 5');
expectTabActive(scope.tabs[2]);
});
it('should deselect all when no tabs are active', function() {
scope.active = 101;
scope.$apply();
expectTabActive(null);
expect(contents().filter('.active').length).toBe(0);
scope.active = 5;
scope.$apply();
expectTabActive(scope.tabs[2]);
});
it('should not select twice', function() {
elm.remove();
elm = null;
scope = $rootScope.$new();
scope.tabs = [
makeTab(2), makeTab(3), makeTab(5), makeTab(8)
];
scope.active = 13;
scope.select = jasmine.createSpy();
elm = $compile([
'',
' ',
' heading {{index}}',
' content {{$index}}',
' ',
' ',
' heading foo',
' content foo',
' ',
''
].join('\n'))(scope);
scope.$apply();
expect(scope.select.calls.count()).toBe(1);
});
});
describe('advanced uib-tab-heading element', function() {
beforeEach(inject(function($compile, $rootScope, $sce) {
scope = $rootScope.$new();
scope.myHtml = $sce.trustAsHtml('hello, there!');
scope.value = true;
elm = $compile([
'',
' ',
' ',
' ',
' 1',
' 2
',
' 3
',
''
].join('\n'))(scope);
scope.$apply();
}));
function heading() {
return elm.find('ul li > a').children();
}
it('should create a heading bound to myHtml', function() {
expect(heading().eq(0).html()).toBe('hello, there!');
});
it('should hide and show the heading depending on value', function() {
expect(heading().eq(0)).not.toBeHidden();
scope.$apply('value = false');
expect(heading().eq(0)).toBeHidden();
scope.$apply('value = true');
expect(heading().eq(0)).not.toBeHidden();
});
it('should have a uib-tab-heading no matter what syntax was used', function() {
expect(heading().eq(1).text()).toBe('1');
expect(heading().eq(2).text()).toBe('2');
expect(heading().eq(3).text()).toBe('3');
});
});
//Tests that http://git.io/lG6I9Q is fixed
describe('tab ordering', function() {
beforeEach(inject(function($compile, $rootScope) {
scope = $rootScope.$new();
scope.tabs = [
{ title:'Title 1', available:true },
{ title:'Title 2', available:true },
{ title:'Title 3', available:true }
];
elm = $compile([
'',
' ',
' div that makes troubles
',
' First Static',
' another div that may do evil
',
' some content',
' ',
' Mid Static',
' a text node',
' ',
' yet another span that may do evil',
' some content',
' a text node',
' yet another span that may do evil',
' ',
' Last Static',
' a text node',
' yet another span that may do evil',
' ',
''
].join('\n'))(scope);
scope.tabIsAvailable = function(tab) {
return tab.available;
};
}));
it('should preserve correct ordering', function() {
function titles() {
return elm.find('ul.nav-tabs li > a');
}
scope.$apply();
expect(titles().length).toBe(9);
scope.$apply('tabs[1].available=false');
scope.$digest();
expect(titles().length).toBe(7);
scope.$apply('tabs[0].available=false');
scope.$digest();
expect(titles().length).toBe(5);
scope.$apply('tabs[2].available=false');
scope.$digest();
expect(titles().length).toBe(3);
scope.$apply('tabs[0].available=true');
scope.$digest();
expect(titles().length).toBe(5);
scope.$apply('tabs[1].available=true');
scope.$apply('tabs[2].available=true');
scope.$digest();
expect(titles().length).toBe(9);
expect(titles().eq(0).text().trim()).toBe('first');
expect(titles().eq(1).text().trim()).toBe('Title 1');
expect(titles().eq(2).text().trim()).toBe('Title 2');
expect(titles().eq(3).text().trim()).toBe('Title 3');
expect(titles().eq(4).text().trim()).toBe('mid');
expect(titles().eq(5).text().trim()).toBe('Second Title 1');
expect(titles().eq(6).text().trim()).toBe('Second Title 2');
expect(titles().eq(7).text().trim()).toBe('Second Title 3');
expect(titles().eq(8).text().trim()).toBe('last');
});
});
describe('uib-tabset controller', function() {
function mockTab(index) {
return {
index: index,
onSelect : angular.noop,
onDeselect : angular.noop
};
}
var ctrl;
beforeEach(inject(function($controller, $rootScope) {
scope = $rootScope;
//instantiate the controller stand-alone, without the directive
ctrl = $controller('UibTabsetController', {$scope: scope});
}));
describe('select', function() {
it('should mark given tab selected', function() {
ctrl.tabs = [
{
tab: mockTab(0),
index: 0
}
];
ctrl.select(0);
expect(ctrl.active).toBe(0);
});
it('should deselect other tabs', function() {
var tab1 = mockTab(1), tab2 = mockTab(2), tab3 = mockTab(3);
ctrl.addTab(tab1);
ctrl.addTab(tab2);
ctrl.addTab(tab3);
ctrl.select(0);
expect(ctrl.active).toBe(1);
ctrl.select(1);
expect(ctrl.active).toBe(2);
ctrl.select(2);
expect(ctrl.active).toBe(3);
});
});
describe('addTab', function() {
it('should append tab', function() {
var tab1 = mockTab(1), tab2 = mockTab(2);
expect(ctrl.tabs).toEqual([]);
ctrl.addTab(tab1);
expect(ctrl.tabs).toEqual([
{
tab: tab1,
index: 1
}
]);
ctrl.addTab(tab2);
expect(ctrl.tabs).toEqual([
{
tab: tab1,
index: 1
},
{
tab: tab2,
index: 2
}
]);
});
it('should select the first one', function() {
var tab1 = mockTab(1), tab2 = mockTab(2);
ctrl.addTab(tab1);
expect(ctrl.active).toBe(1);
ctrl.addTab(tab2);
expect(ctrl.active).toBe(1);
});
it('should not select first active === false tab as selected', function() {
var tab = mockTab(0);
ctrl.active = 1;
ctrl.addTab(tab);
expect(ctrl.active).toBe(1);
});
it('should retain active state when adding tab of different index', function() {
var tab1 = mockTab(1), tab2 = mockTab(2);
ctrl.active = 2;
ctrl.addTab(tab1);
expect(ctrl.active).toBe(2);
ctrl.addTab(tab2);
expect(ctrl.active).toBe(2);
});
});
});
describe('remove', function() {
it('should remove title tabs when elements are destroyed and change selection', inject(function($controller, $compile, $rootScope) {
scope = $rootScope.$new();
elm = $compile('Hellocontent {{i}}')(scope);
scope.$apply();
expectTitles(['1']);
expectContents(['Hello']);
scope.$apply('list = [1,2,3]');
expectTitles(['1', 'tab 1', 'tab 2', 'tab 3']);
expectContents(['Hello', 'content 1', 'content 2', 'content 3']);
// Select last tab
titles().find('> a').eq(3).click();
expect(contents().eq(3)).toHaveClass('active');
expect(titles().eq(3)).toHaveClass('active');
// Remove last tab
scope.$apply('list = [1,2]');
expectTitles(['1', 'tab 1', 'tab 2']);
expectContents(['Hello', 'content 1', 'content 2']);
// "tab 2" is now selected
expect(titles().eq(2)).toHaveClass('active');
expect(contents().eq(2)).toHaveClass('active');
// Select 2nd tab ("tab 1")
titles().find('> a').eq(1).click();
expect(titles().eq(1)).toHaveClass('active');
expect(contents().eq(1)).toHaveClass('active');
// Remove 2nd tab
scope.$apply('list = [2]');
expectTitles(['1', 'tab 2']);
expectContents(['Hello', 'content 2']);
// New 2nd tab is now selected
expect(titles().eq(1)).toHaveClass('active');
expect(contents().eq(1)).toHaveClass('active');
}));
it('should use updated index in tab', inject(function($controller, $compile, $rootScope) {
scope = $rootScope.$new();
elm = $compile('Hellocontent {{i}}')(scope);
scope.$apply();
scope.$apply('list = [1,2,3]');
expectTitles(['1', 'tab 1', 'tab 2', 'tab 3']);
expectContents(['Hello', 'content 1', 'content 2', 'content 3']);
// Remove middle "tab 2" tab
scope.$apply('list = [1,3]');
expectTitles(['1', 'tab 1', 'tab 3']);
expectContents(['Hello', 'content 1', 'content 3']);
// Remove last "tab 3" tab
scope.$apply('list = [1]');
expectTitles(['1', 'tab 1']);
expectContents(['Hello', 'content 1']);
// Select first tab
titles().find('> a').eq(0).click();
expect(titles().eq(0)).toHaveClass('active');
expect(contents().eq(0)).toHaveClass('active');
}));
it('should not select tabs when being destroyed', inject(function($controller, $compile, $rootScope) {
var selectList = [],
deselectList = [],
getTab = function(index) {
return {
index: index,
select: function() {
selectList.push('select');
},
deselect: function() {
deselectList.push('deselect');
}
};
};
scope = $rootScope.$new();
scope.tabs = [
getTab(0),
getTab(1)
];
scope.active = 1;
elm = $compile([
'',
' ',
' heading {{index}}',
' content {{$index}}',
' ',
''
].join('\n'))(scope);
scope.$apply();
// The first tab is selected the during the initial $digest.
expect(selectList.length).toEqual(1);
// Destroy the tabs - we should not trigger selection/deselection any more.
scope.$destroy();
expect(selectList.length).toEqual(1);
expect(deselectList.length).toEqual(0);
}));
});
describe('disable', function() {
beforeEach(inject(function($compile, $rootScope) {
scope = $rootScope.$new();
function makeTab(disable, index) {
return {
index: index,
select: jasmine.createSpy(),
disable: disable
};
}
scope.tabs = [
makeTab(false, 0), makeTab(true, 1), makeTab(false, 2), makeTab(true, 3)
];
scope.active = 1;
elm = $compile([
'',
' ',
' heading {{index}}',
' content {{$index}}',
' ',
''
].join('\n'))(scope);
scope.$apply();
}));
function expectTabActive(activeTab) {
var _titles = titles();
angular.forEach(scope.tabs, function(tab, i) {
if (activeTab === tab) {
expect(scope.active).toBe(tab.index);
expect(tab.select.calls.count()).toBe(tab.disable ? 0 : 1);
expect(_titles.eq(i)).toHaveClass('active');
expect(contents().eq(i).text().trim()).toBe('content ' + i);
expect(contents().eq(i)).toHaveClass('active');
} else {
expect(scope.active).not.toBe(tab.index);
expect(_titles.eq(i)).not.toHaveClass('active');
}
});
}
it('should not switch active when clicking on title', function() {
titles().eq(2).find('> a').click();
expectTabActive(scope.tabs[2]);
titles().eq(3).find('> a').click();
expectTabActive(scope.tabs[2]);
});
it('should toggle between states', function() {
expect(titles().eq(3)).toHaveClass('disabled');
scope.$apply('tabs[3].disable = false');
expect(titles().eq(3)).not.toHaveClass('disabled');
expect(titles().eq(2)).not.toHaveClass('disabled');
scope.$apply('tabs[2].disable = true');
expect(titles().eq(2)).toHaveClass('disabled');
});
});
describe('vertical', function() {
beforeEach(inject(function($compile, $rootScope) {
scope = $rootScope.$new();
scope.vertical = true;
elm = $compile('')(scope);
scope.$apply();
}));
it('to stack tabs', function() {
expect(elm.find('ul.nav-tabs')).toHaveClass('nav-stacked');
});
});
describe('justified', function() {
beforeEach(inject(function($compile, $rootScope) {
scope = $rootScope.$new();
scope.justified = true;
elm = $compile('')(scope);
scope.$apply();
}));
it('to justify tabs', function() {
expect(elm.find('ul.nav-tabs')).toHaveClass('nav-justified');
});
});
describe('type', function() {
beforeEach(inject(function($compile, $rootScope) {
scope = $rootScope.$new();
scope.navType = 'pills';
elm = $compile('')(scope);
scope.$apply();
}));
it('to show pills', function() {
expect(elm.find('ul')).toHaveClass('nav-pills');
expect(elm.find('ul')).not.toHaveClass('nav-tabs');
});
});
//https://github.com/angular-ui/bootstrap/issues/524
describe('child compilation', function() {
var elm;
beforeEach(inject(function($compile, $rootScope) {
elm = $compile('')($rootScope.$new());
$rootScope.$apply();
}));
it('should hookup the tab\'s children to the tab with $compile', function() {
var tabChild = $('.tab-pane', elm).children().first();
expect(tabChild.inheritedData('$uibTabsetController')).toBeTruthy();
});
});
//https://github.com/angular-ui/bootstrap/issues/631
describe('ng-options in content', function() {
var elm;
it('should render correct amount of options', inject(function($compile, $rootScope) {
var scope = $rootScope.$new();
elm = $compile('
')(scope);
scope.$apply();
var select = elm.find('select');
scope.$apply();
expect(select.children().length).toBe(4);
}));
});
//https://github.com/angular-ui/bootstrap/issues/599
describe('ng-repeat in content', function() {
var elm;
it('should render ng-repeat', inject(function($compile, $rootScope) {
var scope = $rootScope.$new();
scope.tabs = [
{title:'a', array:[1,2,3]},
{title:'b', array:[2,3,4]},
{title:'c', array:[3,4,5]}
];
elm = $compile('
' +
'' +
'{{$index}}' +
'{{a}},' +
'' +
'
')(scope);
scope.$apply();
var contents = elm.find('.tab-pane');
expect(contents.eq(0).text().trim()).toEqual('1,2,3,');
expect(contents.eq(1).text().trim()).toEqual('2,3,4,');
expect(contents.eq(2).text().trim()).toEqual('3,4,5,');
}));
});
//https://github.com/angular-ui/bootstrap/issues/783
describe('nested tabs', function() {
var elm;
it('should render without errors', inject(function($compile, $rootScope) {
var scope = $rootScope.$new();
elm = $compile([
'
',
' ',
' ',
' ',
' ',
' ',
' ',
' ',
' ',
' ',
' ',
' ',
' ',
' ',
' ',
'
'
].join('\n'))(scope);
scope.$apply();
// 1 outside tabset, 2 nested tabsets
expect(elm.find('.tabbable').length).toEqual(3);
}));
it('should render with the correct scopes', inject(function($compile, $rootScope) {
var scope = $rootScope.$new();
scope.tab1Text = 'abc';
scope.tab1aText = '123';
scope.tab1aHead = '123';
scope.tab2aaText = '456';
elm = $compile([
'
',
' ',
' ',
' ',
' ',
' {{ tab1aText }}',
' ',
' ',
' {{ tab1Text }}',
' ',
' ',
' ',
' ',
' ',
' ',
' {{ tab2aaText }}',
' ',
' ',
' ',
' ',
' ',
' ',
'
'
].join('\n'))(scope);
scope.$apply();
var outsideTabset = elm.find('.tabbable').eq(0);
var nestedTabset = outsideTabset.find('.tabbable');
expect(elm.find('.tabbable').length).toEqual(4);
expect(outsideTabset.find('.tab-pane').eq(0).find('.tab-1').text().trim()).toEqual(scope.tab1Text);
expect(nestedTabset.find('.tab-pane').eq(0).text().trim()).toEqual(scope.tab1aText);
expect(nestedTabset.find('ul.nav-tabs li').eq(0).text().trim()).toEqual(scope.tab1aHead);
expect(nestedTabset.eq(2).find('.tab-pane').eq(0).find('.tab-2aa').text().trim()).toEqual(scope.tab2aaText);
}));
it('ng-repeat works with nested tabs', inject(function($compile, $rootScope) {
var scope = $rootScope.$new();
scope.tabs = [
{
tabs: [
{
content: 'tab1a'
},
{
content: 'tab2a'
}
],
content: 'tab1'
}
];
elm = $compile([
'
',
' ',
' ',
' ',
' ',
' {{ innerTab.content }}',
' ',
' ',
' {{ tab.content }}',
' ',
' ',
'
'
].join('\n'))(scope);
scope.$apply();
expect(elm.find('.inner-tab-content').eq(0).text().trim()).toEqual(scope.tabs[0].tabs[0].content);
expect(elm.find('.inner-tab-content').eq(1).text().trim()).toEqual(scope.tabs[0].tabs[1].content);
expect(elm.find('.outer-tab-content').eq(0).text().trim()).toEqual(scope.tabs[0].content);
}));
});
});
================================================
FILE: src/timepicker/docs/demo.html
================================================
Time is: {{mytime | date:'shortTime' }}
Hours step is:
Minutes step is:
================================================
FILE: src/timepicker/docs/demo.js
================================================
angular.module('ui.bootstrap.demo').controller('TimepickerDemoCtrl', function ($scope, $log) {
$scope.mytime = new Date();
$scope.hstep = 1;
$scope.mstep = 15;
$scope.options = {
hstep: [1, 2, 3],
mstep: [1, 5, 10, 15, 25, 30]
};
$scope.ismeridian = true;
$scope.toggleMode = function() {
$scope.ismeridian = ! $scope.ismeridian;
};
$scope.update = function() {
var d = new Date();
d.setHours( 14 );
d.setMinutes( 0 );
$scope.mytime = d;
};
$scope.changed = function () {
$log.log('Time changed to: ' + $scope.mytime);
};
$scope.clear = function() {
$scope.mytime = null;
};
});
================================================
FILE: src/timepicker/docs/readme.md
================================================
A lightweight & configurable timepicker directive.
### uib-timepicker settings
* `arrowkeys`
$
C
_(Default: `true`)_ -
Whether user can use up/down arrow keys inside the hours & minutes input to increase or decrease its values.
* `hour-step`
$
C
_(Default: `1`)_ -
Number of hours to increase or decrease when using a button.
* `max`
$
_(Default: `undefined`)_ -
Maximum time a user can select.
* `meridians`
$
C
_(Default: `null`)_ -
Meridian labels based on locale. To override you must supply an array like `['AM', 'PM']`.
* `min`
$
_(Default: `undefined`)_ -
Minimum time a user can select
* `minute-step`
$
C
_(Default: `1`)_ -
Number of minutes to increase or decrease when using a button.
* `mousewheel`
$
C
_(Default: `true`)_ -
Whether user can scroll inside the hours & minutes input to increase or decrease its values.
* `ng-disabled`
$
_(Default: `false`)_ -
Whether or not to disable the component.
* `ng-model`
$
-
Date object that provides the time state.
* `pad-hours`
$
_(Default: true)_ -
Whether the hours column is padded with a 0.
* `readonly-input`
$
C
_(Default: `false`)_ -
Whether user can type inside the hours & minutes input.
* `second-step`
$
C
_(Default: `1`)_ -
Number of seconds to increase or decrease when using a button.
* `show-meridian`
$
C
_(Default: `true`)_ -
Whether to display 12H or 24H mode.
* `show-seconds`
$
C
_(Default: `false`)_ -
Show seconds input.
* `show-spinners`
$
C
_(Default: `true`)_ -
Show spinner arrows above and below the inputs.
* `tabindex`
_(Defaults: `0`)_ -
Sets tabindex for each control in the timepicker.
* `template-url`
C
_(Defaults: `uib/template/timepicker/timepicker.html`)_ -
Add the ability to override the template used on the component.
**Notes**
This component makes no claims of absolutely supporting the preservation of dates in all cases, and it is highly recommended that model tracking of dates is encapsulated in a different object. This component should not be used with the same model as the datepicker. This is due to edge cases with situations such as Daylight Savings timezone changes which require a modification of the date in order to prevent an impossible to increment or decrement situation. See [#5485](https://github.com/angular-ui/bootstrap/issues/5485) for details.
If the model value is updated (i.e. via `Date.prototype.setDate`), you must update the model value by breaking the reference by `modelValue = new Date(modelValue)` in order to have the timepicker update.
================================================
FILE: src/timepicker/index-nocss.js
================================================
require('../../template/timepicker/timepicker.html.js');
require('./timepicker');
var MODULE_NAME = 'ui.bootstrap.module.timepicker';
angular.module(MODULE_NAME, ['ui.bootstrap.timepicker', 'uib/template/timepicker/timepicker.html']);
module.exports = MODULE_NAME;
================================================
FILE: src/timepicker/index.js
================================================
require('./timepicker.css');
module.exports = require('./index-nocss.js');
================================================
FILE: src/timepicker/test/timepicker.spec.js
================================================
describe('timepicker directive', function() {
var $rootScope, $compile, $templateCache, element, modelCtrl;
beforeEach(module('ui.bootstrap.timepicker'));
beforeEach(module('uib/template/timepicker/timepicker.html'));
beforeEach(inject(function(_$compile_, _$rootScope_, _$templateCache_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
$rootScope.time = newTime(14, 40, 25);
$templateCache = _$templateCache_;
element = $compile('
')($rootScope);
$rootScope.$digest();
modelCtrl = element.controller('ngModel');
}));
function newTime(hours, minutes, seconds) {
seconds = seconds ? seconds : 0;
var time = new Date();
time.setHours(hours);
time.setMinutes(minutes);
time.setSeconds(seconds);
//this is required, otherwise rollover edges cases tests will have
//time reset to dates that are off by milliseconds.
time.setMilliseconds(0);
return time;
}
function getTimeState(withoutMeridian, withoutSeconds) {
var inputs = element.find('input');
var limit = withoutSeconds ? 2 : 3;
var state = [];
for (var i = 0; i < limit; i ++) {
state.push(inputs.eq(i).val());
}
if (withoutMeridian !== true) {
state.push(getMeridianButton().text());
}
return state;
}
function getModelState(withoutSeconds) {
if (withoutSeconds) {
return [$rootScope.time.getHours(), $rootScope.time.getMinutes()];
}
return [$rootScope.time.getHours(), $rootScope.time.getMinutes(), $rootScope.time.getSeconds()];
}
function getArrow(isUp, tdIndex) {
return element.find('tr').eq(isUp ? 0 : 2).find('td').eq(tdIndex).find('a').eq(0);
}
function getHoursButton(isUp) {
return getArrow(isUp, 0);
}
function getMinutesButton(isUp) {
return getArrow(isUp, 2);
}
function getSecondsButton(isUp) {
return getArrow(isUp, 4);
}
function getMeridianButton() {
return element.find('button').eq(0);
}
function doClick(button, n) {
for (var i = 0, max = n || 1; i < max; i++) {
button.click();
$rootScope.$digest();
}
}
function wheelThatMouse(delta) {
var e = $.Event('mousewheel');
e.wheelDelta = delta;
return e;
}
function wheelThatOtherMouse(delta) {
var e = $.Event('wheel');
e.deltaY = delta;
return e;
}
function keydown(key) {
var e = $.Event('keydown');
switch(key) {
case 'left':
e.which = 37;
break;
case 'up':
e.which = 38;
break;
case 'right':
e.which = 39;
break;
case 'down':
e.which = 40;
break;
}
return e;
}
it('contains three row & four input elements', function() {
expect(element.find('tr').length).toBe(3);
expect(element.find('input').length).toBe(3);
expect(element.find('button').length).toBe(1);
});
it('has initially the correct time & meridian', function() {
expect(getTimeState()).toEqual(['02', '40', '25', 'PM']);
expect(getModelState()).toEqual([14, 40, 25]);
});
it('should be pristine', function() {
expect(modelCtrl.$pristine).toBe(true);
});
it('should be untouched', function() {
expect(modelCtrl.$untouched).toBe(true);
});
it('has `selected` current time when model is initially cleared', function() {
$rootScope.time = null;
element = $compile('
')($rootScope);
$rootScope.$digest();
expect($rootScope.time).toBe(null);
expect(getTimeState()).not.toEqual(['', '', '' , '']);
});
it('changes inputs when model changes value', function() {
$rootScope.time = newTime(11, 50, 20);
$rootScope.$digest();
expect(getTimeState()).toEqual(['11', '50', '20', 'AM']);
expect(getModelState()).toEqual([11, 50, 20]);
$rootScope.time = newTime(16, 40, 45);
$rootScope.$digest();
expect(getTimeState()).toEqual(['04', '40', '45', 'PM']);
expect(getModelState()).toEqual([16, 40, 45]);
});
it('increases / decreases hours when arrows are clicked', function() {
var up = getHoursButton(true);
var down = getHoursButton(false);
doClick(up);
expect(getTimeState()).toEqual(['03', '40', '25', 'PM']);
expect(getModelState()).toEqual([15, 40, 25]);
doClick(down);
expect(getTimeState()).toEqual(['02', '40', '25', 'PM']);
expect(getModelState()).toEqual([14, 40, 25]);
doClick(down);
expect(getTimeState()).toEqual(['01', '40', '25', 'PM']);
expect(getModelState()).toEqual([13, 40, 25]);
});
it('increase / decreases minutes by default step when arrows are clicked', function() {
var up = getMinutesButton(true);
var down = getMinutesButton(false);
doClick(up);
expect(getTimeState()).toEqual(['02', '41', '25', 'PM']);
expect(getModelState()).toEqual([14, 41, 25]);
doClick(down);
expect(getTimeState()).toEqual(['02', '40', '25', 'PM']);
expect(getModelState()).toEqual([14, 40, 25]);
doClick(down);
expect(getTimeState()).toEqual(['02', '39', '25', 'PM']);
expect(getModelState()).toEqual([14, 39, 25]);
});
it('increase / decreases seconds by default step when arrows are clicked', function() {
var up = getSecondsButton(true);
var down = getSecondsButton(false);
doClick(up);
expect(getTimeState()).toEqual(['02', '40', '26', 'PM']);
expect(getModelState()).toEqual([14, 40, 26]);
doClick(down);
expect(getTimeState()).toEqual(['02', '40', '25', 'PM']);
expect(getModelState()).toEqual([14, 40, 25]);
doClick(down);
expect(getTimeState()).toEqual(['02', '40', '24', 'PM']);
expect(getModelState()).toEqual([14, 40, 24]);
});
it('should be dirty when input changes', function() {
var upHours = getHoursButton(true);
var upMinutes = getMinutesButton(true);
var upSeconds = getSecondsButton(true);
doClick(upHours);
expect(modelCtrl.$dirty).toBe(true);
modelCtrl.$setPristine();
doClick(upMinutes);
expect(modelCtrl.$dirty).toBe(true);
modelCtrl.$setPristine();
doClick(upSeconds);
expect(modelCtrl.$dirty).toBe(true);
});
it('should be touched when input blurs', function() {
var inputs = element.find('input');
var hoursInput = inputs.eq(0),
minutesInput = inputs.eq(1),
secondsInput = inputs.eq(2);
hoursInput.val(12);
$rootScope.$digest();
hoursInput.blur();
expect(modelCtrl.$touched).toBe(true);
modelCtrl.$setUntouched();
minutesInput.val(20);
$rootScope.$digest();
hoursInput.blur();
expect(modelCtrl.$touched).toBe(true);
modelCtrl.$setUntouched();
secondsInput.val(9);
$rootScope.$digest();
hoursInput.blur();
expect(modelCtrl.$touched).toBe(true);
});
it('meridian button has correct type', function() {
var button = getMeridianButton();
expect(button.attr('type')).toBe('button');
});
it('toggles meridian when button is clicked', function() {
var button = getMeridianButton();
doClick(button);
expect(getTimeState()).toEqual(['02', '40', '25', 'AM']);
expect(getModelState()).toEqual([2, 40, 25]);
doClick(button);
expect(getTimeState()).toEqual(['02', '40', '25', 'PM']);
expect(getModelState()).toEqual([14, 40, 25]);
doClick(button);
expect(getTimeState()).toEqual(['02', '40', '25', 'AM']);
expect(getModelState()).toEqual([2, 40, 25]);
});
it('has minutes "connected" to hours', function() {
var up = getMinutesButton(true);
var down = getMinutesButton(false);
doClick(up, 10);
expect(getTimeState()).toEqual(['02', '50', '25', 'PM']);
expect(getModelState()).toEqual([14, 50, 25]);
doClick(up, 10);
expect(getTimeState()).toEqual(['03', '00', '25', 'PM']);
expect(getModelState()).toEqual([15, 0, 25]);
doClick(up, 10);
$rootScope.$digest();
expect(getTimeState()).toEqual(['03', '10', '25', 'PM']);
expect(getModelState()).toEqual([15, 10, 25]);
doClick(down, 10);
$rootScope.$digest();
expect(getTimeState()).toEqual(['03', '00', '25', 'PM']);
expect(getModelState()).toEqual([15, 0, 25]);
doClick(down, 10);
$rootScope.$digest();
expect(getTimeState()).toEqual(['02', '50', '25', 'PM']);
expect(getModelState()).toEqual([14, 50, 25]);
});
it('has seconds "connected" to minutes', function() {
var up = getSecondsButton(true);
var down = getSecondsButton(false);
doClick(up, 15);
expect(getTimeState()).toEqual(['02', '40', '40', 'PM']);
expect(getModelState()).toEqual([14, 40, 40]);
doClick(up, 15);
expect(getTimeState()).toEqual(['02', '40', '55', 'PM']);
expect(getModelState()).toEqual([14, 40, 55]);
doClick(up, 15);
$rootScope.$digest();
expect(getTimeState()).toEqual(['02', '41', '10', 'PM']);
expect(getModelState()).toEqual([14, 41, 10]);
doClick(down, 15);
$rootScope.$digest();
expect(getTimeState()).toEqual(['02', '40', '55', 'PM']);
expect(getModelState()).toEqual([14, 40, 55]);
doClick(down, 15);
$rootScope.$digest();
expect(getTimeState()).toEqual(['02', '40', '40', 'PM']);
expect(getModelState()).toEqual([14, 40, 40]);
});
it('has hours "connected" to meridian', function() {
var up = getHoursButton(true);
var down = getHoursButton(false);
// AM -> PM
$rootScope.time = newTime(11, 0, 25);
$rootScope.$digest();
expect(getTimeState()).toEqual(['11', '00', '25', 'AM']);
expect(getModelState()).toEqual([11, 0, 25]);
doClick(up);
expect(getTimeState()).toEqual(['12', '00', '25', 'PM']);
expect(getModelState()).toEqual([12, 0, 25]);
doClick(up);
expect(getTimeState()).toEqual(['01', '00', '25', 'PM']);
expect(getModelState()).toEqual([13, 0, 25]);
doClick(down);
expect(getTimeState()).toEqual(['12', '00', '25', 'PM']);
expect(getModelState()).toEqual([12, 0, 25]);
doClick(down);
expect(getTimeState()).toEqual(['11', '00', '25', 'AM']);
expect(getModelState()).toEqual([11, 0, 25]);
// PM -> AM
$rootScope.time = newTime(23, 0, 25);
$rootScope.$digest();
expect(getTimeState()).toEqual(['11', '00', '25', 'PM']);
expect(getModelState()).toEqual([23, 0, 25]);
doClick(up);
expect(getTimeState()).toEqual(['12', '00', '25', 'AM']);
expect(getModelState()).toEqual([0, 0, 25]);
doClick(up);
expect(getTimeState()).toEqual(['01', '00', '25', 'AM']);
expect(getModelState()).toEqual([1, 0, 25]);
doClick(down);
expect(getTimeState()).toEqual(['12', '00', '25', 'AM']);
expect(getModelState()).toEqual([0, 0, 25]);
doClick(down);
expect(getTimeState()).toEqual(['11', '00', '25', 'PM']);
expect(getModelState()).toEqual([23, 0, 25]);
});
it('changes only the time part when hours change', function() {
$rootScope.time = newTime(23, 50, 20);
$rootScope.$digest();
var date = $rootScope.time.getDate();
var up = getHoursButton(true);
doClick(up);
expect(getTimeState()).toEqual(['12', '50', '20', 'AM']);
expect(getModelState()).toEqual([0, 50, 20]);
expect(date).toEqual($rootScope.time.getDate());
});
it('changes only the time part when minutes change', function() {
element = $compile('
')($rootScope);
$rootScope.time = newTime(0, 0, 0);
$rootScope.$digest();
var date = $rootScope.time.getDate();
var up = getMinutesButton(true);
doClick(up, 2);
expect(getTimeState()).toEqual(['12', '30', '00', 'AM']);
expect(getModelState()).toEqual([0, 30, 0]);
expect(date).toEqual($rootScope.time.getDate());
var down = getMinutesButton(false);
doClick(down, 2);
expect(getTimeState()).toEqual(['12', '00', '00', 'AM']);
expect(getModelState()).toEqual([0, 0, 0]);
expect(date).toEqual($rootScope.time.getDate());
doClick(down, 2);
expect(getTimeState()).toEqual(['11', '30', '00', 'PM']);
expect(getModelState()).toEqual([23, 30, 0]);
expect(date).toEqual($rootScope.time.getDate());
});
it('responds properly on "mousewheel" events', function() {
var inputs = element.find('input');
var hoursEl = inputs.eq(0), minutesEl = inputs.eq(1), secondsEl = inputs.eq(2);
var upMouseWheelEvent = wheelThatMouse(1);
var downMouseWheelEvent = wheelThatMouse(-1);
expect(getTimeState()).toEqual(['02', '40', '25', 'PM']);
expect(getModelState()).toEqual([14, 40, 25]);
// UP
hoursEl.trigger(upMouseWheelEvent);
$rootScope.$digest();
expect(getTimeState()).toEqual(['03', '40', '25', 'PM']);
expect(getModelState()).toEqual([15, 40, 25]);
hoursEl.trigger(upMouseWheelEvent);
$rootScope.$digest();
expect(getTimeState()).toEqual(['04', '40', '25', 'PM']);
expect(getModelState()).toEqual([16, 40, 25]);
minutesEl.trigger(upMouseWheelEvent);
$rootScope.$digest();
expect(getTimeState()).toEqual(['04', '41', '25', 'PM']);
expect(getModelState()).toEqual([16, 41, 25]);
minutesEl.trigger(upMouseWheelEvent);
$rootScope.$digest();
expect(getTimeState()).toEqual(['04', '42', '25', 'PM']);
expect(getModelState()).toEqual([16, 42, 25]);
secondsEl.trigger(upMouseWheelEvent);
$rootScope.$digest();
expect(getTimeState()).toEqual(['04', '42', '26', 'PM']);
expect(getModelState()).toEqual([16, 42, 26]);
secondsEl.trigger(upMouseWheelEvent);
$rootScope.$digest();
expect(getTimeState()).toEqual(['04', '42', '27', 'PM']);
expect(getModelState()).toEqual([16, 42, 27]);
// DOWN
secondsEl.trigger(downMouseWheelEvent);
$rootScope.$digest();
expect(getTimeState()).toEqual(['04', '42', '26', 'PM']);
expect(getModelState()).toEqual([16, 42, 26]);
secondsEl.trigger(downMouseWheelEvent);
$rootScope.$digest();
expect(getTimeState()).toEqual(['04', '42', '25', 'PM']);
expect(getModelState()).toEqual([16, 42, 25]);
minutesEl.trigger(downMouseWheelEvent);
$rootScope.$digest();
expect(getTimeState()).toEqual(['04', '41', '25', 'PM']);
expect(getModelState()).toEqual([16, 41, 25]);
minutesEl.trigger(downMouseWheelEvent);
$rootScope.$digest();
expect(getTimeState()).toEqual(['04', '40', '25', 'PM']);
expect(getModelState()).toEqual([16, 40, 25]);
hoursEl.trigger(downMouseWheelEvent);
$rootScope.$digest();
expect(getTimeState()).toEqual(['03', '40', '25', 'PM']);
expect(getModelState()).toEqual([15, 40, 25]);
hoursEl.trigger(downMouseWheelEvent);
$rootScope.$digest();
expect(getTimeState()).toEqual(['02', '40', '25', 'PM']);
expect(getModelState()).toEqual([14, 40, 25]);
});
it('responds properly on "wheel" events', function() {
var inputs = element.find('input');
var hoursEl = inputs.eq(0), minutesEl = inputs.eq(1), secondsEl = inputs.eq(2);
var upMouseWheelEvent = wheelThatOtherMouse(-1);
var downMouseWheelEvent = wheelThatOtherMouse(1);
expect(getTimeState()).toEqual(['02', '40', '25', 'PM']);
expect(getModelState()).toEqual([14, 40, 25]);
// UP
hoursEl.trigger(upMouseWheelEvent);
$rootScope.$digest();
expect(getTimeState()).toEqual(['03', '40', '25', 'PM']);
expect(getModelState()).toEqual([15, 40, 25]);
hoursEl.trigger(upMouseWheelEvent);
$rootScope.$digest();
expect(getTimeState()).toEqual(['04', '40', '25', 'PM']);
expect(getModelState()).toEqual([16, 40, 25]);
minutesEl.trigger(upMouseWheelEvent);
$rootScope.$digest();
expect(getTimeState()).toEqual(['04', '41', '25', 'PM']);
expect(getModelState()).toEqual([16, 41, 25]);
minutesEl.trigger(upMouseWheelEvent);
$rootScope.$digest();
expect(getTimeState()).toEqual(['04', '42', '25', 'PM']);
expect(getModelState()).toEqual([16, 42, 25]);
secondsEl.trigger(upMouseWheelEvent);
$rootScope.$digest();
expect(getTimeState()).toEqual(['04', '42', '26', 'PM']);
expect(getModelState()).toEqual([16, 42, 26]);
secondsEl.trigger(upMouseWheelEvent);
$rootScope.$digest();
expect(getTimeState()).toEqual(['04', '42', '27', 'PM']);
expect(getModelState()).toEqual([16, 42, 27]);
// DOWN
secondsEl.trigger(downMouseWheelEvent);
$rootScope.$digest();
expect(getTimeState()).toEqual(['04', '42', '26', 'PM']);
expect(getModelState()).toEqual([16, 42, 26]);
secondsEl.trigger(downMouseWheelEvent);
$rootScope.$digest();
expect(getTimeState()).toEqual(['04', '42', '25', 'PM']);
expect(getModelState()).toEqual([16, 42, 25]);
minutesEl.trigger(downMouseWheelEvent);
$rootScope.$digest();
expect(getTimeState()).toEqual(['04', '41', '25', 'PM']);
expect(getModelState()).toEqual([16, 41, 25]);
minutesEl.trigger(downMouseWheelEvent);
$rootScope.$digest();
expect(getTimeState()).toEqual(['04', '40', '25', 'PM']);
expect(getModelState()).toEqual([16, 40, 25]);
hoursEl.trigger(downMouseWheelEvent);
$rootScope.$digest();
expect(getTimeState()).toEqual(['03', '40', '25', 'PM']);
expect(getModelState()).toEqual([15, 40, 25]);
hoursEl.trigger(downMouseWheelEvent);
$rootScope.$digest();
expect(getTimeState()).toEqual(['02', '40', '25', 'PM']);
expect(getModelState()).toEqual([14, 40, 25]);
});
it('responds properly on "keydown" events', function() {
var inputs = element.find('input');
var hoursEl = inputs.eq(0), minutesEl = inputs.eq(1),
secondsEl = inputs.eq(2);
var upKeydownEvent = keydown('up');
var downKeydownEvent = keydown('down');
var leftKeydownEvent = keydown('left');
expect(getTimeState()).toEqual(['02', '40', '25', 'PM']);
expect(getModelState()).toEqual([14, 40, 25]);
// UP
hoursEl.trigger(upKeydownEvent);
$rootScope.$digest();
expect(getTimeState()).toEqual(['03', '40', '25', 'PM']);
expect(getModelState()).toEqual([15, 40, 25]);
hoursEl.trigger(upKeydownEvent);
$rootScope.$digest();
expect(getTimeState()).toEqual(['04', '40', '25', 'PM']);
expect(getModelState()).toEqual([16, 40, 25]);
minutesEl.trigger(upKeydownEvent);
$rootScope.$digest();
expect(getTimeState()).toEqual(['04', '41', '25', 'PM']);
expect(getModelState()).toEqual([16, 41, 25]);
minutesEl.trigger(upKeydownEvent);
$rootScope.$digest();
expect(getTimeState()).toEqual(['04', '42', '25', 'PM']);
expect(getModelState()).toEqual([16, 42, 25]);
secondsEl.trigger(upKeydownEvent);
$rootScope.$digest();
expect(getTimeState()).toEqual(['04', '42', '26', 'PM']);
expect(getModelState()).toEqual([16, 42, 26]);
secondsEl.trigger(upKeydownEvent);
$rootScope.$digest();
expect(getTimeState()).toEqual(['04', '42', '27', 'PM']);
expect(getModelState()).toEqual([16, 42, 27]);
// DOWN
secondsEl.trigger(downKeydownEvent);
$rootScope.$digest();
expect(getTimeState()).toEqual(['04', '42', '26', 'PM']);
expect(getModelState()).toEqual([16, 42, 26]);
secondsEl.trigger(downKeydownEvent);
$rootScope.$digest();
expect(getTimeState()).toEqual(['04', '42', '25', 'PM']);
expect(getModelState()).toEqual([16, 42, 25]);
minutesEl.trigger(downKeydownEvent);
$rootScope.$digest();
expect(getTimeState()).toEqual(['04', '41', '25', 'PM']);
expect(getModelState()).toEqual([16, 41, 25]);
minutesEl.trigger(downKeydownEvent);
$rootScope.$digest();
expect(getTimeState()).toEqual(['04', '40', '25', 'PM']);
expect(getModelState()).toEqual([16, 40, 25]);
hoursEl.trigger(downKeydownEvent);
$rootScope.$digest();
expect(getTimeState()).toEqual(['03', '40', '25', 'PM']);
expect(getModelState()).toEqual([15, 40, 25]);
hoursEl.trigger(downKeydownEvent);
$rootScope.$digest();
expect(getTimeState()).toEqual(['02', '40', '25', 'PM']);
expect(getModelState()).toEqual([14, 40, 25]);
// Other keydown
hoursEl.trigger(leftKeydownEvent);
$rootScope.$digest();
expect(getTimeState()).toEqual(['02', '40', '25', 'PM']);
expect(getModelState()).toEqual([14, 40, 25]);
minutesEl.trigger(leftKeydownEvent);
$rootScope.$digest();
expect(getTimeState()).toEqual(['02', '40', '25', 'PM']);
expect(getModelState()).toEqual([14, 40, 25]);
secondsEl.trigger(leftKeydownEvent);
$rootScope.$digest();
expect(getTimeState()).toEqual(['02', '40', '25', 'PM']);
expect(getModelState()).toEqual([14, 40, 25]);
});
describe('attributes', function() {
beforeEach(function() {
$rootScope.hstep = 2;
$rootScope.mstep = 30;
$rootScope.sstep = 30;
$rootScope.time = newTime(14, 0 , 0);
element = $compile('
')($rootScope);
$rootScope.$digest();
});
it('increases / decreases hours by configurable step', function() {
var up = getHoursButton(true);
var down = getHoursButton(false);
expect(getTimeState()).toEqual(['02', '00', '00', 'PM']);
expect(getModelState()).toEqual([14, 0, 0]);
doClick(up);
expect(getTimeState()).toEqual(['04', '00', '00', 'PM']);
expect(getModelState()).toEqual([16, 0, 0]);
doClick(down);
expect(getTimeState()).toEqual(['02', '00', '00', 'PM']);
expect(getModelState()).toEqual([14, 0, 0]);
doClick(down);
expect(getTimeState()).toEqual(['12', '00', '00', 'PM']);
expect(getModelState()).toEqual([12, 0, 0]);
// Change step
$rootScope.hstep = 3;
$rootScope.$digest();
doClick(up);
expect(getTimeState()).toEqual(['03', '00', '00', 'PM']);
expect(getModelState()).toEqual([15, 0, 0]);
doClick(down);
expect(getTimeState()).toEqual(['12', '00', '00', 'PM']);
expect(getModelState()).toEqual([12, 0, 0]);
});
it('increases / decreases minutes by configurable step', function() {
var up = getMinutesButton(true);
var down = getMinutesButton(false);
doClick(up);
expect(getTimeState()).toEqual(['02', '30', '00', 'PM']);
expect(getModelState()).toEqual([14, 30, 0]);
doClick(up);
expect(getTimeState()).toEqual(['03', '00', '00', 'PM']);
expect(getModelState()).toEqual([15, 0, 0]);
doClick(down);
expect(getTimeState()).toEqual(['02', '30', '00', 'PM']);
expect(getModelState()).toEqual([14, 30, 0]);
doClick(down);
expect(getTimeState()).toEqual(['02', '00', '00', 'PM']);
expect(getModelState()).toEqual([14, 0, 0]);
// Change step
$rootScope.mstep = 15;
$rootScope.$digest();
doClick(up);
expect(getTimeState()).toEqual(['02', '15', '00', 'PM']);
expect(getModelState()).toEqual([14, 15, 0]);
doClick(down);
expect(getTimeState()).toEqual(['02', '00', '00', 'PM']);
expect(getModelState()).toEqual([14, 0, 0]);
doClick(down);
expect(getTimeState()).toEqual(['01', '45', '00', 'PM']);
expect(getModelState()).toEqual([13, 45, 0]);
});
it('responds properly on "mousewheel" events with configurable steps', function() {
var inputs = element.find('input');
var hoursEl = inputs.eq(0), minutesEl = inputs.eq(1), secondsEl = inputs.eq(2);
var upMouseWheelEvent = wheelThatMouse(1);
var downMouseWheelEvent = wheelThatMouse(-1);
expect(getTimeState()).toEqual(['02', '00', '00', 'PM']);
expect(getModelState()).toEqual([14, 0, 0]);
// UP
hoursEl.trigger(upMouseWheelEvent);
$rootScope.$digest();
expect(getTimeState()).toEqual(['04', '00', '00', 'PM']);
expect(getModelState()).toEqual([16, 0, 0]);
minutesEl.trigger(upMouseWheelEvent);
$rootScope.$digest();
expect(getTimeState()).toEqual(['04', '30', '00', 'PM']);
expect(getModelState()).toEqual([16, 30, 0]);
secondsEl.trigger(upMouseWheelEvent);
$rootScope.$digest();
expect(getTimeState()).toEqual(['04', '30', '30', 'PM']);
expect(getModelState()).toEqual([16, 30, 30]);
// DOWN
secondsEl.trigger(downMouseWheelEvent);
$rootScope.$digest();
expect(getTimeState()).toEqual(['04', '30', '00', 'PM']);
expect(getModelState()).toEqual([16, 30, 0]);
minutesEl.trigger(downMouseWheelEvent);
$rootScope.$digest();
expect(getTimeState()).toEqual(['04', '00', '00', 'PM']);
expect(getModelState()).toEqual([16, 0, 0]);
hoursEl.trigger(downMouseWheelEvent);
$rootScope.$digest();
expect(getTimeState()).toEqual(['02', '00', '00', 'PM']);
expect(getModelState()).toEqual([14, 0, 0]);
});
it('responds properly on "wheel" events with configurable steps', function() {
var inputs = element.find('input');
var hoursEl = inputs.eq(0), minutesEl = inputs.eq(1), secondsEl = inputs.eq(2);
var upMouseWheelEvent = wheelThatOtherMouse(-1);
var downMouseWheelEvent = wheelThatOtherMouse(1);
expect(getTimeState()).toEqual(['02', '00', '00', 'PM']);
expect(getModelState()).toEqual([14, 0, 0]);
// UP
hoursEl.trigger(upMouseWheelEvent);
$rootScope.$digest();
expect(getTimeState()).toEqual(['04', '00', '00', 'PM']);
expect(getModelState()).toEqual([16, 0, 0]);
minutesEl.trigger(upMouseWheelEvent);
$rootScope.$digest();
expect(getTimeState()).toEqual(['04', '30', '00', 'PM']);
expect(getModelState()).toEqual([16, 30, 0]);
secondsEl.trigger(upMouseWheelEvent);
$rootScope.$digest();
expect(getTimeState()).toEqual(['04', '30', '30', 'PM']);
expect(getModelState()).toEqual([16, 30, 30]);
// DOWN
secondsEl.trigger(downMouseWheelEvent);
$rootScope.$digest();
expect(getTimeState()).toEqual(['04', '30', '00', 'PM']);
expect(getModelState()).toEqual([16, 30, 0]);
minutesEl.trigger(downMouseWheelEvent);
$rootScope.$digest();
expect(getTimeState()).toEqual(['04', '00', '00', 'PM']);
expect(getModelState()).toEqual([16, 0, 0]);
hoursEl.trigger(downMouseWheelEvent);
$rootScope.$digest();
expect(getTimeState()).toEqual(['02', '00', '00', 'PM']);
expect(getModelState()).toEqual([14, 0, 0]);
});
it('can handle strings as steps', function() {
var upHours = getHoursButton(true);
var upMinutes = getMinutesButton(true);
var upSeconds = getSecondsButton(true);
expect(getTimeState()).toEqual(['02', '00', '00', 'PM']);
expect(getModelState()).toEqual([14, 0, 0]);
$rootScope.hstep = '4';
$rootScope.mstep = '20';
$rootScope.sstep = '20';
$rootScope.$digest();
doClick(upHours);
expect(getTimeState()).toEqual(['06', '00', '00', 'PM']);
expect(getModelState()).toEqual([18, 0, 0]);
doClick(upMinutes);
expect(getTimeState()).toEqual(['06', '20', '00', 'PM']);
expect(getModelState()).toEqual([18, 20, 0]);
doClick(upSeconds);
expect(getTimeState()).toEqual(['06', '20', '20', 'PM']);
expect(getModelState()).toEqual([18, 20, 20]);
});
});
describe('without seconds mode',function(){
beforeEach(function(){
$rootScope.displaysSeconds = false;
$rootScope.time = newTime(14,40,35);
element = $compile('
')($rootScope);
$rootScope.$digest();
});
it('increases / decreases hours when arrows are clicked', function() {
var up = getHoursButton(true);
var down = getHoursButton(false);
doClick(up);
expect(getTimeState(false, true)).toEqual(['03', '40', 'PM']);
expect(getModelState(true)).toEqual([15, 40]);
doClick(down);
expect(getTimeState(false, true)).toEqual(['02', '40', 'PM']);
expect(getModelState(true)).toEqual([14, 40]);
doClick(down);
expect(getTimeState(false, true)).toEqual(['01', '40', 'PM']);
expect(getModelState(true)).toEqual([13, 40]);
});
it('increase / decreases minutes by default step when arrows are clicked', function() {
var up = getMinutesButton(true);
var down = getMinutesButton(false);
doClick(up);
expect(getTimeState(false, true)).toEqual(['02', '41', 'PM']);
expect(getModelState(true)).toEqual([14, 41]);
doClick(down);
expect(getTimeState(false, true)).toEqual(['02', '40', 'PM']);
expect(getModelState(true)).toEqual([14, 40]);
doClick(down);
expect(getTimeState(false, true)).toEqual(['02', '39', 'PM']);
expect(getModelState(true)).toEqual([14, 39]);
});
it('has minutes "connected" to hours', function() {
var up = getMinutesButton(true);
var down = getMinutesButton(false);
doClick(up, 10);
expect(getTimeState(false, true)).toEqual(['02', '50', 'PM']);
expect(getModelState(true)).toEqual([14, 50]);
doClick(up, 10);
expect(getTimeState(false, true)).toEqual(['03', '00', 'PM']);
expect(getModelState(true)).toEqual([15, 0]);
doClick(up, 10);
$rootScope.$digest();
expect(getTimeState(false, true)).toEqual(['03', '10', 'PM']);
expect(getModelState(true)).toEqual([15, 10]);
doClick(down, 10);
$rootScope.$digest();
expect(getTimeState(false, true)).toEqual(['03', '00', 'PM']);
expect(getModelState(true)).toEqual([15, 0]);
doClick(down, 10);
$rootScope.$digest();
expect(getTimeState(false,true)).toEqual(['02', '50', 'PM']);
expect(getModelState(true)).toEqual([14, 50]);
});
});
describe('12 / 24 hour mode', function() {
beforeEach(function() {
$rootScope.meridian = false;
$rootScope.time = newTime(14, 10, 20);
element = $compile('
')($rootScope);
$rootScope.$digest();
});
function getMeridianTd() {
return element.find('tr').eq(1).find('td').eq(5);
}
it('initially displays correct time when `show-meridian` is false', function() {
expect(getTimeState(true)).toEqual(['14', '10', '20']);
expect(getModelState()).toEqual([14, 10, 20]);
expect(getMeridianTd()).toBeHidden();
});
it('toggles correctly between different modes', function() {
expect(getTimeState(true)).toEqual(['14', '10', '20']);
$rootScope.meridian = true;
$rootScope.$digest();
expect(getTimeState()).toEqual(['02', '10', '20', 'PM']);
expect(getModelState()).toEqual([14, 10, 20]);
expect(getMeridianTd()).not.toBeHidden();
$rootScope.meridian = false;
$rootScope.$digest();
expect(getTimeState(true)).toEqual(['14', '10', '20']);
expect(getModelState()).toEqual([14, 10, 20]);
expect(getMeridianTd()).toBeHidden();
});
it('handles correctly initially empty model on parent element', function() {
$rootScope.time = null;
element = $compile('
')($rootScope);
$rootScope.$digest();
expect($rootScope.time).toBe(null);
});
});
describe('`meridians` attribute', function() {
beforeEach(inject(function() {
$rootScope.meridiansArray = ['am', 'pm'];
element = $compile('
')($rootScope);
$rootScope.$digest();
}));
it('displays correctly', function() {
expect(getTimeState()[3]).toBe('pm');
});
it('toggles correctly', function() {
$rootScope.time = newTime(2, 40, 20);
$rootScope.$digest();
expect(getTimeState()[3]).toBe('am');
});
});
describe('`readonly-input` attribute', function() {
beforeEach(inject(function() {
$rootScope.meridiansArray = ['am', 'pm'];
element = $compile('
')($rootScope);
$rootScope.$digest();
}));
it('should make inputs readonly', function() {
var inputs = element.find('input');
expect(inputs.eq(0).attr('readonly')).toBe('readonly');
expect(inputs.eq(1).attr('readonly')).toBe('readonly');
expect(inputs.eq(2).attr('readonly')).toBe('readonly');
});
});
describe('`pad-hours` attribute', function() {
function triggerInput(elem, val) {
elem.val(val);
elem.trigger('input');
}
it('should pad the hours by default', function() {
element = $compile('
')($rootScope);
$rootScope.$digest();
var inputs = element.find('input');
var hoursInput = inputs.eq(0);
triggerInput(hoursInput, 4);
hoursInput.blur();
expect(hoursInput.val()).toBe('04');
});
it('should not pad the hours', function() {
element = $compile('
')($rootScope);
$rootScope.$digest();
var inputs = element.find('input');
var hoursInput = inputs.eq(0);
triggerInput(hoursInput, 4);
hoursInput.blur();
expect(hoursInput.val()).toBe('4');
});
});
describe('setting uibTimepickerConfig steps', function() {
var originalConfig = {};
beforeEach(inject(function(_$compile_, _$rootScope_, uibTimepickerConfig) {
angular.extend(originalConfig, uibTimepickerConfig);
uibTimepickerConfig.hourStep = 2;
uibTimepickerConfig.minuteStep = 10;
uibTimepickerConfig.secondStep = 10;
uibTimepickerConfig.showMeridian = false;
element = $compile('
')($rootScope);
$rootScope.$digest();
}));
afterEach(inject(function(uibTimepickerConfig) {
// return it to the original state
angular.extend(uibTimepickerConfig, originalConfig);
}));
it('does not affect the initial value', function() {
expect(getTimeState(true)).toEqual(['14', '40', '25']);
expect(getModelState()).toEqual([14, 40, 25]);
});
it('increases / decreases hours with configured step', function() {
var up = getHoursButton(true);
var down = getHoursButton(false);
doClick(up, 2);
expect(getTimeState(true)).toEqual(['18', '40', '25']);
expect(getModelState()).toEqual([18, 40, 25]);
doClick(down, 3);
expect(getTimeState(true)).toEqual(['12', '40', '25']);
expect(getModelState()).toEqual([12, 40, 25]);
});
it('increases / decreases minutes with configured step', function() {
var up = getMinutesButton(true);
var down = getMinutesButton(false);
doClick(up);
expect(getTimeState(true)).toEqual(['14', '50', '25']);
expect(getModelState()).toEqual([14, 50, 25]);
doClick(down, 3);
expect(getTimeState(true)).toEqual(['14', '20' , '25']);
expect(getModelState()).toEqual([14, 20, 25]);
});
it('increases / decreases seconds with configured step', function() {
var up = getSecondsButton(true);
var down = getSecondsButton(false);
doClick(up);
expect(getTimeState(true)).toEqual(['14', '40', '35']);
expect(getModelState()).toEqual([14, 40, 35]);
doClick(down, 3);
expect(getTimeState(true)).toEqual(['14', '40', '05']);
expect(getModelState()).toEqual([14, 40, 5]);
});
});
describe('setting uibTimepickerConfig meridian labels', function() {
var originalConfig = {};
beforeEach(inject(function(_$compile_, _$rootScope_, uibTimepickerConfig) {
angular.extend(originalConfig, uibTimepickerConfig);
uibTimepickerConfig.meridians = ['π.μ.', 'μ.μ.'];
uibTimepickerConfig.showMeridian = true;
element = $compile('
')($rootScope);
$rootScope.$digest();
}));
afterEach(inject(function(uibTimepickerConfig) {
// return it to the original state
angular.extend(uibTimepickerConfig, originalConfig);
}));
it('displays correctly', function() {
expect(getTimeState()).toEqual(['02', '40', '25', 'μ.μ.']);
expect(getModelState()).toEqual([14, 40, 25]);
});
it('toggles correctly', function() {
$rootScope.time = newTime(2, 40, 20);
$rootScope.$digest();
expect(getTimeState()).toEqual(['02', '40', '20', 'π.μ.']);
expect(getModelState()).toEqual([2, 40, 20]);
});
});
describe('setting uibTimepickerConfig template url', function() {
var originalConfig = {};
var newTemplateUrl = 'foo/bar.html';
beforeEach(inject(function(_$compile_, _$rootScope_, uibTimepickerConfig) {
angular.extend(originalConfig, uibTimepickerConfig);
$templateCache.put(newTemplateUrl, '
baz
');
uibTimepickerConfig.templateUrl = newTemplateUrl;
element = $compile('
')($rootScope);
$rootScope.$digest();
}));
afterEach(inject(function(uibTimepickerConfig) {
// return it to the original state
angular.extend(uibTimepickerConfig, originalConfig);
}));
it('should use a custom template', function() {
expect(element[0].tagName.toLowerCase()).toBe('div');
expect(element.html()).toBe('
baz
');
});
});
describe('$formatter', function() {
var ngModel,
date;
beforeEach(function() {
ngModel = element.controller('ngModel');
date = new Date('Mon Mar 23 2015 14:40:11 GMT-0700 (PDT)');
});
it('should have one formatter', function() {
expect(ngModel.$formatters.length).toBe(1);
});
it('should convert a date to a new reference representing the same date', function() {
expect(ngModel.$formatters[0](date)).toEqual(date);
});
it('should convert a valid date string to a date object', function() {
expect(ngModel.$formatters[0]('Mon Mar 23 2015 14:40:11 GMT-0700 (PDT)')).toEqual(date);
});
it('should set falsy values as null', function() {
expect(ngModel.$formatters[0](undefined)).toBe(null);
expect(ngModel.$formatters[0](null)).toBe(null);
expect(ngModel.$formatters[0]('')).toBe(null);
expect(ngModel.$formatters[0](0)).toBe(null);
expect(ngModel.$formatters[0](NaN)).toBe(null);
});
});
describe('user input validation', function() {
var changeInputValueTo;
beforeEach(inject(function($sniffer) {
changeInputValueTo = function(inputEl, value) {
inputEl.val(value);
inputEl.trigger($sniffer.hasEvent('input') ? 'input' : 'change');
$rootScope.$digest();
};
}));
function getHoursInputEl() {
return element.find('input').eq(0);
}
function getMinutesInputEl() {
return element.find('input').eq(1);
}
function getSecondsInputEl() {
return element.find('input').eq(2);
}
it('has initially the correct time & meridian', function() {
expect(getTimeState()).toEqual(['02', '40', '25', 'PM']);
expect(getModelState()).toEqual([14, 40, 25]);
});
it('updates hours & pads on input change & pads on blur', function() {
var el = getHoursInputEl();
changeInputValueTo(el, 5);
expect(getTimeState()).toEqual(['5', '40', '25', 'PM']);
expect(getModelState()).toEqual([17, 40, 25]);
el.blur();
expect(getTimeState()).toEqual(['05', '40', '25', 'PM']);
expect(getModelState()).toEqual([17, 40, 25]);
});
it('updates minutes & pads on input change & pads on blur', function() {
var el = getMinutesInputEl();
changeInputValueTo(el, 9);
expect(getTimeState()).toEqual(['02', '9', '25', 'PM']);
expect(getModelState()).toEqual([14, 9, 25]);
el.blur();
expect(getTimeState()).toEqual(['02', '09', '25', 'PM']);
expect(getModelState()).toEqual([14, 9, 25]);
});
it('updates seconds & pads on input change & pads on blur', function() {
var el = getSecondsInputEl();
changeInputValueTo(el, 4);
expect(getTimeState()).toEqual(['02', '40', '4', 'PM']);
expect(getModelState()).toEqual([14, 40, 4]);
el.blur();
expect(getTimeState()).toEqual(['02', '40', '04', 'PM']);
expect(getModelState()).toEqual([14, 40, 4]);
});
it('clears model when input hours is invalid & alerts the UI', function() {
var el = getHoursInputEl();
changeInputValueTo(el, 'pizza');
expect($rootScope.time).toBe(null);
expect(el.parent().hasClass('has-error')).toBe(true);
expect(el.hasClass('ng-invalid-hours'));
expect(element.hasClass('ng-invalid-time')).toBe(true);
changeInputValueTo(el, 8);
el.blur();
$rootScope.$digest();
expect(getTimeState()).toEqual(['08', '40', '25', 'PM']);
expect(getModelState()).toEqual([20, 40, 25]);
expect(el.parent().hasClass('has-error')).toBe(false);
expect(element.hasClass('ng-invalid-time')).toBe(false);
});
it('clears model when input minutes is invalid & alerts the UI', function() {
var el = getMinutesInputEl();
changeInputValueTo(el, '8a');
expect($rootScope.time).toBe(null);
expect(el.parent().hasClass('has-error')).toBe(true);
expect(el.hasClass('ng-invalid-minutes'));
expect(element.hasClass('ng-invalid-time')).toBe(true);
changeInputValueTo(el, 22);
expect(getTimeState()).toEqual(['02', '22', '25', 'PM']);
expect(getModelState()).toEqual([14, 22, 25]);
expect(el.parent().hasClass('has-error')).toBe(false);
expect(element.hasClass('ng-invalid-time')).toBe(false);
});
it('clears model when input seconds is invalid & alerts the UI', function() {
var el = getSecondsInputEl();
changeInputValueTo(el, 'pizza');
expect($rootScope.time).toBe(null);
expect(el.parent().hasClass('has-error')).toBe(true);
expect(el.hasClass('ng-invalid-seconds'));
expect(element.hasClass('ng-invalid-time')).toBe(true);
changeInputValueTo(el, 13);
expect(getTimeState()).toEqual(['02', '40', '13', 'PM']);
expect(getModelState()).toEqual([14, 40, 13]);
expect(el.parent().hasClass('has-error')).toBe(false);
expect(element.hasClass('ng-invalid-time')).toBe(false);
});
it('should not be invalid when the model is cleared', function() {
var elH = getHoursInputEl();
var elM = getMinutesInputEl();
var elS = getSecondsInputEl();
$rootScope.time = newTime(10, 20, 30);
$rootScope.$digest();
expect(getModelState()).toEqual([10, 20, 30]);
changeInputValueTo(elH, '');
elH.blur();
$rootScope.$digest();
changeInputValueTo(elM, '');
elM.blur();
$rootScope.$digest();
changeInputValueTo(elS, '');
elS.blur();
$rootScope.$digest();
expect(elH.hasClass('ng-valid'));
expect(elM.hasClass('ng-valid'));
expect(elS.hasClass('ng-valid'));
expect(element.hasClass('ng-invalid-time')).toBe(false);
});
it('timepicker1 leaves view alone when hours are invalid and minutes are updated', function() {
var hoursEl = getHoursInputEl(),
minutesEl = getMinutesInputEl();
changeInputValueTo(hoursEl, '25');
hoursEl.blur();
$rootScope.$digest();
expect(getTimeState()).toEqual(['25', '40', '25', 'PM']);
changeInputValueTo(minutesEl, '2');
minutesEl.blur();
$rootScope.$digest();
expect(getTimeState()).toEqual(['25', '2', '25', 'PM']);
});
it('leaves view alone when minutes are invalid and hours are updated', function() {
var hoursEl = getHoursInputEl(),
minutesEl = getMinutesInputEl();
changeInputValueTo(minutesEl, '61');
minutesEl.blur();
$rootScope.$digest();
expect($rootScope.time).toBe(null);
expect(getTimeState()).toEqual(['02', '61', '25', 'PM']);
changeInputValueTo(hoursEl, '2');
hoursEl.blur();
$rootScope.$digest();
expect($rootScope.time).toBe(null);
expect(getTimeState()).toEqual(['2', '61', '25', 'PM']);
});
it('handles 12/24H mode change', function() {
$rootScope.meridian = true;
element = $compile('
')($rootScope);
$rootScope.$digest();
var el = getHoursInputEl();
changeInputValueTo(el, '16');
expect($rootScope.time).toBe(null);
expect(el.parent().hasClass('has-error')).toBe(true);
expect(element.hasClass('ng-invalid-time')).toBe(true);
$rootScope.meridian = false;
$rootScope.$digest();
expect(getTimeState(true)).toEqual(['16', '40', '25']);
expect(getModelState()).toEqual([16, 40, 25]);
expect(element.hasClass('ng-invalid-time')).toBe(false);
});
it('should have a default tabindex of 0', function() {
element = $compile('
')($rootScope);
$rootScope.$digest();
expect(element.isolateScope().tabindex).toBe(0);
});
it('should have the correct tabindex', function() {
element = $compile('
')($rootScope);
$rootScope.$digest();
expect(element.attr('tabindex')).toBe(undefined);
expect(element.isolateScope().tabindex).toBe('5');
});
});
describe('when model is not a Date', function() {
beforeEach(inject(function() {
element = $compile('
')($rootScope);
}));
it('should not be invalid when the model is null', function() {
$rootScope.time = null;
$rootScope.$digest();
expect(element.hasClass('ng-invalid-time')).toBe(false);
});
it('should not be invalid when the model is undefined', function() {
$rootScope.time = undefined;
$rootScope.$digest();
expect(element.hasClass('ng-invalid-time')).toBe(false);
});
it('should not be invalid when the model is a valid string date representation', function() {
$rootScope.time = 'September 30, 2010 15:30:10';
$rootScope.$digest();
expect(element.hasClass('ng-invalid-time')).toBe(false);
expect(getTimeState()).toEqual(['03', '30', '10','PM']);
});
it('should be invalid when the model is not a valid string date representation', function() {
$rootScope.time = 'pizza';
$rootScope.$digest();
expect(element.hasClass('ng-invalid-time')).toBe(true);
});
it('should return valid when the model becomes valid', function() {
$rootScope.time = 'pizza';
$rootScope.$digest();
expect(element.hasClass('ng-invalid-time')).toBe(true);
$rootScope.time = new Date();
$rootScope.$digest();
expect(element.hasClass('ng-invalid-time')).toBe(false);
});
it('should return valid when the model is cleared', function() {
$rootScope.time = 'pizza';
$rootScope.$digest();
expect(element.hasClass('ng-invalid-time')).toBe(true);
$rootScope.time = null;
$rootScope.$digest();
expect(element.hasClass('ng-invalid-time')).toBe(false);
});
});
describe('use with `ng-required` directive', function() {
beforeEach(inject(function() {
$rootScope.time = null;
element = $compile('
')($rootScope);
$rootScope.$digest();
}));
it('should be invalid initially', function() {
expect(element.hasClass('ng-invalid')).toBe(true);
});
it('should be valid if model has been specified', function() {
$rootScope.time = new Date();
$rootScope.$digest();
expect(element.hasClass('ng-invalid')).toBe(false);
});
});
describe('use with `ng-change` directive', function() {
beforeEach(inject(function() {
$rootScope.changeHandler = jasmine.createSpy('changeHandler');
$rootScope.time = new Date();
element = $compile('
')($rootScope);
$rootScope.$digest();
}));
it('should not be called initially', function() {
expect($rootScope.changeHandler).not.toHaveBeenCalled();
});
it('should be called when hours / minutes buttons clicked', function() {
var btn1 = getHoursButton(true);
var btn2 = getMinutesButton(false);
var btn3 = getSecondsButton(false);
doClick(btn1, 2);
doClick(btn2, 3);
doClick(btn3, 1);
$rootScope.$digest();
expect($rootScope.changeHandler.calls.count()).toBe(6);
});
it('should not be called when model changes programatically', function() {
$rootScope.time = new Date();
$rootScope.$digest();
expect($rootScope.changeHandler).not.toHaveBeenCalled();
});
});
describe('when used with min', function() {
var changeInputValueTo;
beforeEach(inject(function($sniffer) {
element = $compile('
')($rootScope);
$rootScope.$digest();
changeInputValueTo = function(inputEl, value) {
inputEl.val(value);
inputEl.trigger($sniffer.hasEvent('input') ? 'input' : 'change');
$rootScope.$digest();
};
}));
it('should not decrease hours when it would result in a time earlier than min', function() {
var down = getHoursButton(false);
var inputs = element.find('input');
var hoursEl = inputs.eq(0);
var downMouseWheelEvent = wheelThatMouse(-1);
var downKeydownEvent = keydown('down');
$rootScope.min = newTime(13, 41);
$rootScope.$digest();
expect(down.hasClass('disabled')).toBe(true);
doClick(down);
expect(getTimeState(false,true)).toEqual(['02', '40', 'PM']);
expect(getModelState(true)).toEqual([14, 40]);
hoursEl.trigger(downMouseWheelEvent);
$rootScope.$digest();
expect(getTimeState(false, true)).toEqual(['02', '40', 'PM']);
expect(getModelState(true)).toEqual([14, 40]);
hoursEl.trigger(downKeydownEvent);
$rootScope.$digest();
expect(getTimeState(false, true)).toEqual(['02', '40', 'PM']);
expect(getModelState(true)).toEqual([14, 40]);
});
it('should decrease hours when it would not result in a time earlier than min', function() {
var down = getHoursButton(false);
var inputs = element.find('input');
var hoursEl = inputs.eq(0);
var downMouseWheelEvent = wheelThatMouse(-1);
var downKeydownEvent = keydown('down');
$rootScope.min = newTime(0, 0);
$rootScope.$digest();
expect(down.hasClass('disabled')).toBe(false);
doClick(down);
expect(getTimeState(false, true)).toEqual(['01', '40', 'PM']);
expect(getModelState(true)).toEqual([13, 40]);
hoursEl.trigger(downMouseWheelEvent);
$rootScope.$digest();
expect(getTimeState(false, true)).toEqual(['12', '40', 'PM']);
expect(getModelState(true)).toEqual([12, 40]);
hoursEl.trigger(downKeydownEvent);
$rootScope.$digest();
expect(getTimeState(false, true)).toEqual(['11', '40', 'AM']);
expect(getModelState(true)).toEqual([11, 40]);
});
it('should not decrease minutes when it would result in a time ealier than min', function() {
var down = getMinutesButton(false);
var inputs = element.find('input');
var minutesEl = inputs.eq(1);
var downMouseWheelEvent = wheelThatMouse(-1);
var downKeydownEvent = keydown('down');
$rootScope.min = newTime(14, 40);
$rootScope.$digest();
expect(down.hasClass('disabled')).toBe(true);
doClick(down);
expect(getTimeState(false, true)).toEqual(['02', '40', 'PM']);
expect(getModelState(true)).toEqual([14, 40]);
minutesEl.trigger(downMouseWheelEvent);
$rootScope.$digest();
expect(getTimeState(false, true)).toEqual(['02', '40', 'PM']);
expect(getModelState(true)).toEqual([14, 40]);
minutesEl.trigger(downKeydownEvent);
$rootScope.$digest();
expect(getTimeState(false, true)).toEqual(['02', '40', 'PM']);
expect(getModelState(true)).toEqual([14, 40]);
});
it('should decrease minutes when it would not result in a time ealier than min', function() {
var down = getMinutesButton(false);
var inputs = element.find('input');
var minutesEl = inputs.eq(1);
var downMouseWheelEvent = wheelThatMouse(-1);
var downKeydownEvent = keydown('down');
$rootScope.min = newTime(0, 0);
$rootScope.$digest();
expect(down.hasClass('disabled')).toBe(false);
doClick(down);
expect(getTimeState(false, true)).toEqual(['02', '39', 'PM']);
expect(getModelState(true)).toEqual([14, 39]);
minutesEl.trigger(downMouseWheelEvent);
$rootScope.$digest();
expect(getTimeState(false, true)).toEqual(['02', '38', 'PM']);
expect(getModelState(true)).toEqual([14, 38]);
minutesEl.trigger(downKeydownEvent);
$rootScope.$digest();
expect(getTimeState(false, true)).toEqual(['02', '37', 'PM']);
expect(getModelState(true)).toEqual([14, 37]);
});
it('should not increase hours when time would rollover to a time earlier than min', function() {
var up = getHoursButton(true);
var inputs = element.find('input');
var hoursEl = inputs.eq(0);
var upMouseWheelEvent = wheelThatMouse(1);
var upKeydownEvent = keydown('up');
$rootScope.time = newTime(23, 59);
$rootScope.min = newTime(13, 40);
$rootScope.$digest();
expect(up.hasClass('disabled')).toBe(true);
doClick(up);
expect(getTimeState(false, true)).toEqual(['11', '59', 'PM']);
expect(getModelState(true)).toEqual([23, 59]);
hoursEl.trigger(upMouseWheelEvent);
$rootScope.$digest();
expect(getTimeState(false, true)).toEqual(['11', '59', 'PM']);
expect(getModelState(true)).toEqual([23, 59]);
hoursEl.trigger(upKeydownEvent);
$rootScope.$digest();
expect(getTimeState(false, true)).toEqual(['11', '59', 'PM']);
expect(getModelState(true)).toEqual([23, 59]);
});
it('should increase hours when time would rollover to a time not earlier than min', function() {
var up = getHoursButton(true);
var inputs = element.find('input');
var hoursEl = inputs.eq(0);
var upMouseWheelEvent = wheelThatMouse(1);
var upKeydownEvent = keydown('up');
$rootScope.min = newTime(0, 0);
$rootScope.time = newTime(23, 59);
$rootScope.$digest();
expect(up.hasClass('disabled')).toBe(false);
doClick(up);
expect(getTimeState(false, true)).toEqual(['12', '59', 'AM']);
expect(getModelState(true)).toEqual([0, 59]);
$rootScope.time = newTime(23, 59);
$rootScope.$digest();
hoursEl.trigger(upMouseWheelEvent);
$rootScope.$digest();
expect(getTimeState(false, true)).toEqual(['12', '59', 'AM']);
expect(getModelState(true)).toEqual([0, 59]);
$rootScope.time = newTime(23, 59);
$rootScope.$digest();
hoursEl.trigger(upKeydownEvent);
$rootScope.$digest();
expect(getTimeState(false, true)).toEqual(['12', '59', 'AM']);
expect(getModelState(true)).toEqual([0, 59]);
});
it('should not increase minutes when time would rollover to a time earlier than min', function() {
var up = getMinutesButton(true);
var inputs = element.find('input');
var minutesEl = inputs.eq(1);
var upMouseWheelEvent = wheelThatMouse(1);
var upKeydownEvent = keydown('up');
$rootScope.time = newTime(23, 59);
$rootScope.min = newTime(13, 40);
$rootScope.$digest();
expect(up.hasClass('disabled')).toBe(true);
doClick(up);
expect(getTimeState(false, true)).toEqual(['11', '59', 'PM']);
expect(getModelState(true)).toEqual([23, 59]);
minutesEl.trigger(upMouseWheelEvent);
$rootScope.$digest();
expect(getTimeState(false, true)).toEqual(['11', '59', 'PM']);
expect(getModelState(true)).toEqual([23, 59]);
minutesEl.trigger(upKeydownEvent);
$rootScope.$digest();
expect(getTimeState(false, true)).toEqual(['11', '59', 'PM']);
expect(getModelState(true)).toEqual([23, 59]);
});
it('should increase minutes when time would rollover to a time not earlier than min', function() {
var up = getMinutesButton(true);
var inputs = element.find('input');
var minutesEl = inputs.eq(1);
var upMouseWheelEvent = wheelThatMouse(1);
var upKeydownEvent = keydown('up');
$rootScope.min = newTime(0, 0);
$rootScope.time = newTime(23, 59);
$rootScope.$digest();
expect(up.hasClass('disabled')).toBe(false);
doClick(up);
expect(getTimeState(false, true)).toEqual(['12', '00', 'AM']);
expect(getModelState(true)).toEqual([0, 0]);
$rootScope.time = newTime(23, 59);
$rootScope.$digest();
minutesEl.trigger(upMouseWheelEvent);
$rootScope.$digest();
expect(getTimeState(false, true)).toEqual(['12', '00', 'AM']);
expect(getModelState(true)).toEqual([0, 0]);
$rootScope.time = newTime(23, 59);
$rootScope.$digest();
minutesEl.trigger(upKeydownEvent);
$rootScope.$digest();
expect(getTimeState(false, true)).toEqual(['12', '00', 'AM']);
expect(getModelState(true)).toEqual([0, 0]);
});
it('should not change meridian when it would result a in time earlier than min', function() {
var button = getMeridianButton();
$rootScope.min = newTime(2, 41);
$rootScope.$digest();
expect(button.hasClass('disabled')).toBe(true);
doClick(button);
expect(getTimeState(false, true)).toEqual(['02', '40', 'PM']);
expect(getModelState(true)).toEqual([14, 40]);
});
it('should change meridian when it would not result in a time earlier than min', function() {
var button = getMeridianButton();
$rootScope.min = newTime(2, 39);
$rootScope.$digest();
expect(button.hasClass('disabled')).toBe(false);
doClick(button);
expect(getTimeState(false, true)).toEqual(['02', '40', 'AM']);
expect(getModelState(true)).toEqual([2, 40]);
});
it('should return invalid when the hours are changes such that the time is earlier than min', function() {
var inputs = element.find('input');
var hoursEl = inputs.eq(0);
$rootScope.min = newTime(14, 0);
$rootScope.$digest();
changeInputValueTo(hoursEl, 1);
expect($rootScope.time).toBe(null);
expect(hoursEl.parent().hasClass('has-error')).toBe(true);
expect(element.hasClass('ng-invalid-time')).toBe(true);
});
it('should return valid when the hours are changes such that the time is not earlier than min', function() {
var inputs = element.find('input');
var hoursEl = inputs.eq(0);
$rootScope.min = newTime(14, 41);
$rootScope.$digest();
changeInputValueTo(hoursEl, 3);
expect(getTimeState(false, true)).toEqual(['3', '40', 'PM']);
expect(getModelState(true)).toEqual([15, 40]);
expect(hoursEl.parent().hasClass('has-error')).toBe(false);
expect(element.hasClass('ng-invalid-time')).toBe(false);
});
it('should return invalid when the minutes are changes such that the time is earlier than min', function() {
var inputs = element.find('input');
var minutesEl = inputs.eq(1);
$rootScope.min = newTime(14, 30);
$rootScope.$digest();
changeInputValueTo(minutesEl, 1);
expect($rootScope.time).toBe(null);
expect(minutesEl.parent().hasClass('has-error')).toBe(true);
expect(element.hasClass('ng-invalid-time')).toBe(true);
});
it('should return valid when the minutes are changes such that the time is not earlier than min', function() {
var inputs = element.find('input');
var minutesEl = inputs.eq(1);
$rootScope.min = newTime(14, 41);
$rootScope.$digest();
changeInputValueTo(minutesEl, 42);
expect(getTimeState(false, true)).toEqual(['02', '42', 'PM']);
expect(getModelState(true)).toEqual([14, 42]);
expect(minutesEl.parent().hasClass('has-error')).toBe(false);
expect(element.hasClass('ng-invalid-time')).toBe(false);
});
});
describe('when used with max', function() {
var changeInputValueTo;
beforeEach(inject(function($sniffer) {
element = $compile('
')($rootScope);
$rootScope.$digest();
changeInputValueTo = function(inputEl, value) {
inputEl.val(value);
inputEl.trigger($sniffer.hasEvent('input') ? 'input' : 'change');
$rootScope.$digest();
};
}));
it('should not increase hours when it would result in a time later than max', function() {
var up = getHoursButton(true);
var inputs = element.find('input');
var hoursEl = inputs.eq(0);
var upMouseWheelEvent = wheelThatMouse(1);
var upKeydownEvent = keydown('up');
$rootScope.max = newTime(15, 39);
$rootScope.$digest();
expect(up.hasClass('disabled')).toBe(true);
doClick(up);
expect(getTimeState(false, true)).toEqual(['02', '40', 'PM']);
expect(getModelState(true)).toEqual([14, 40]);
hoursEl.trigger(upMouseWheelEvent);
$rootScope.$digest();
expect(getTimeState(false, true)).toEqual(['02', '40', 'PM']);
expect(getModelState(true)).toEqual([14, 40]);
hoursEl.trigger(upKeydownEvent);
$rootScope.$digest();
expect(getTimeState(false, true)).toEqual(['02', '40', 'PM']);
expect(getModelState(true)).toEqual([14, 40]);
});
it('should increase hours when it would not result in a time later than max', function() {
var up = getHoursButton(true);
var inputs = element.find('input');
var hoursEl = inputs.eq(0);
var upMouseWheelEvent = wheelThatMouse(1);
var upKeydownEvent = keydown('up');
$rootScope.max = newTime(23, 59);
$rootScope.$digest();
expect(up.hasClass('disabled')).toBe(false);
doClick(up);
expect(getTimeState(false, true)).toEqual(['03', '40', 'PM']);
expect(getModelState(true)).toEqual([15, 40]);
hoursEl.trigger(upMouseWheelEvent);
$rootScope.$digest();
expect(getTimeState(false, true)).toEqual(['04', '40', 'PM']);
expect(getModelState(true)).toEqual([16, 40]);
hoursEl.trigger(upKeydownEvent);
$rootScope.$digest();
expect(getTimeState(false, true)).toEqual(['05', '40', 'PM']);
expect(getModelState(true)).toEqual([17, 40]);
});
it('should not increase minutes when it would result in a time later than max', function() {
var up = getMinutesButton(true);
var inputs = element.find('input');
var minutesEl = inputs.eq(1);
var upMouseWheelEvent = wheelThatMouse(1);
var upKeydownEvent = keydown('up');
$rootScope.max = newTime(14, 40);
$rootScope.$digest();
expect(up.hasClass('disabled')).toBe(true);
doClick(up);
expect(getTimeState(false, true)).toEqual(['02', '40', 'PM']);
expect(getModelState(true)).toEqual([14, 40]);
minutesEl.trigger(upMouseWheelEvent);
$rootScope.$digest();
expect(getTimeState(false, true)).toEqual(['02', '40', 'PM']);
expect(getModelState(true)).toEqual([14, 40]);
minutesEl.trigger(upKeydownEvent);
$rootScope.$digest();
expect(getTimeState(false, true)).toEqual(['02', '40', 'PM']);
expect(getModelState(true)).toEqual([14, 40]);
});
it('should increase minutes when it would not result in a time later than max', function() {
var up = getMinutesButton(true);
var inputs = element.find('input');
var minutesEl = inputs.eq(1);
var upMouseWheelEvent = wheelThatMouse(1);
var upKeydownEvent = keydown('up');
$rootScope.max = newTime(23, 59);
$rootScope.$digest();
expect(up.hasClass('disabled')).toBe(false);
doClick(up);
expect(getTimeState(false, true)).toEqual(['02', '41', 'PM']);
expect(getModelState(true)).toEqual([14, 41]);
minutesEl.trigger(upMouseWheelEvent);
$rootScope.$digest();
expect(getTimeState(false, true)).toEqual(['02', '42', 'PM']);
expect(getModelState(true)).toEqual([14, 42]);
minutesEl.trigger(upKeydownEvent);
$rootScope.$digest();
expect(getTimeState(false, true)).toEqual(['02', '43', 'PM']);
expect(getModelState(true)).toEqual([14, 43]);
});
it('should not decrease hours when time would rollover to a time later than max', function() {
var down = getHoursButton(false);
var inputs = element.find('input');
var hoursEl = inputs.eq(0);
var downMouseWheelEvent = wheelThatMouse(-1);
var downKeydownEvent = keydown('down');
$rootScope.time = newTime(0, 0);
$rootScope.max = newTime(13, 40);
$rootScope.$digest();
expect(down.hasClass('disabled')).toBe(true);
doClick(down);
expect(getTimeState(false, true)).toEqual(['12', '00', 'AM']);
expect(getModelState(true)).toEqual([0, 0]);
hoursEl.trigger(downMouseWheelEvent);
$rootScope.$digest();
expect(getTimeState(false, true)).toEqual(['12', '00', 'AM']);
expect(getModelState(true)).toEqual([0, 0]);
hoursEl.trigger(downKeydownEvent);
$rootScope.$digest();
expect(getTimeState(false, true)).toEqual(['12', '00', 'AM']);
expect(getModelState(true)).toEqual([0, 0]);
});
it('should decrease hours when time would rollover to a time not later than max', function() {
var down = getHoursButton(false);
var inputs = element.find('input');
var hoursEl = inputs.eq(0);
var downMouseWheelEvent = wheelThatMouse(-1);
var downKeydownEvent = keydown('down');
$rootScope.max = newTime(23, 59);
$rootScope.time = newTime(0, 0);
$rootScope.$digest();
expect(down.hasClass('disabled')).toBe(false);
doClick(down);
expect(getTimeState(false, true)).toEqual(['11', '00', 'PM']);
expect(getModelState(true)).toEqual([23, 0]);
$rootScope.time = newTime(0, 0);
$rootScope.$digest();
hoursEl.trigger(downMouseWheelEvent);
$rootScope.$digest();
expect(getTimeState(false, true)).toEqual(['11', '00', 'PM']);
expect(getModelState(true)).toEqual([23, 0]);
$rootScope.time = newTime(0, 0);
$rootScope.$digest();
hoursEl.trigger(downKeydownEvent);
$rootScope.$digest();
expect(getTimeState(false, true)).toEqual(['11', '00', 'PM']);
expect(getModelState(true)).toEqual([23, 0]);
});
it('should not decrease minutes when time would rollover to a time later than max', function() {
var down = getMinutesButton(false);
var inputs = element.find('input');
var minutesEl = inputs.eq(1);
var downMouseWheelEvent = wheelThatMouse(-1);
var downKeydownEvent = keydown('down');
$rootScope.time = newTime(0, 0);
$rootScope.max = newTime(13, 40);
$rootScope.$digest();
expect(down.hasClass('disabled')).toBe(true);
doClick(down);
expect(getTimeState(false, true)).toEqual(['12', '00', 'AM']);
expect(getModelState(true)).toEqual([0, 0]);
minutesEl.trigger(downMouseWheelEvent);
$rootScope.$digest();
expect(getTimeState(false, true)).toEqual(['12', '00', 'AM']);
expect(getModelState(true)).toEqual([0, 0]);
minutesEl.trigger(downKeydownEvent);
$rootScope.$digest();
expect(getTimeState(false, true)).toEqual(['12', '00', 'AM']);
expect(getModelState(true)).toEqual([0, 0]);
});
it('should decrease minutes when time would rollover to a time not later than max', function() {
var down = getMinutesButton(false);
var inputs = element.find('input');
var minutesEl = inputs.eq(1);
var downMouseWheelEvent = wheelThatMouse(-1);
var downKeydownEvent = keydown('down');
$rootScope.max = newTime(23, 59);
$rootScope.time = newTime(0, 0);
$rootScope.$digest();
expect(down.hasClass('disabled')).toBe(false);
doClick(down);
expect(getTimeState(false, true)).toEqual(['11', '59', 'PM']);
expect(getModelState(true)).toEqual([23, 59]);
$rootScope.time = newTime(0, 0);
$rootScope.$digest();
minutesEl.trigger(downMouseWheelEvent);
$rootScope.$digest();
expect(getTimeState(false, true)).toEqual(['11', '59', 'PM']);
expect(getModelState(true)).toEqual([23, 59]);
$rootScope.time = newTime(0, 0);
$rootScope.$digest();
minutesEl.trigger( downKeydownEvent );
$rootScope.$digest();
expect(getTimeState(false, true)).toEqual(['11', '59', 'PM']);
expect(getModelState(true)).toEqual([23, 59]);
});
it('should not change meridian when it would result a in time later than max', function() {
var button = getMeridianButton();
$rootScope.time = newTime(2, 40);
$rootScope.max = newTime(14, 39);
$rootScope.$digest();
expect(button.hasClass('disabled')).toBe(true);
doClick(button);
expect(getTimeState(false, true)).toEqual(['02', '40', 'AM']);
expect(getModelState(true)).toEqual([2, 40]);
});
it('should change meridian when it would not result in a time later than max', function() {
var button = getMeridianButton();
$rootScope.time = newTime(2, 40);
$rootScope.max = newTime(14, 41);
$rootScope.$digest();
expect(button.hasClass('disabled')).toBe(false);
doClick(button);
expect(getTimeState(false, true)).toEqual(['02', '40', 'PM']);
expect(getModelState(true)).toEqual([14, 40]);
});
it('should return invalid when the hours are changes such that the time is later than max', function() {
var inputs = element.find('input');
var hoursEl = inputs.eq(0);
$rootScope.max = newTime(14, 0);
$rootScope.$digest();
changeInputValueTo(hoursEl, 3);
expect($rootScope.time).toBe(null);
expect(hoursEl.parent().hasClass('has-error')).toBe(true);
expect(element.hasClass('ng-invalid-time')).toBe(true);
});
it('should return valid when the hours are changes such that the time is not later than max', function() {
var inputs = element.find('input');
var hoursEl = inputs.eq(0);
$rootScope.max = newTime(15, 41);
$rootScope.$digest();
changeInputValueTo(hoursEl, 3);
expect(getTimeState(false, true)).toEqual(['3', '40', 'PM']);
expect(getModelState(true)).toEqual([15, 40]);
expect(hoursEl.parent().hasClass('has-error')).toBe(false);
expect(element.hasClass('ng-invalid-time')).toBe(false);
});
it('should return invalid when the minutes are changes such that the time is later than max', function() {
var inputs = element.find('input');
var minutesEl = inputs.eq(1);
$rootScope.max = newTime(14, 50);
$rootScope.$digest();
changeInputValueTo(minutesEl, 51);
expect($rootScope.time).toBe(null);
expect(minutesEl.parent().hasClass('has-error')).toBe(true);
expect(element.hasClass('ng-invalid-time')).toBe(true);
});
it('should return valid when the minutes are changes such that the time is not later than max', function() {
var inputs = element.find('input');
var minutesEl = inputs.eq(1);
$rootScope.max = newTime(14, 42);
$rootScope.$digest();
changeInputValueTo(minutesEl, 41);
expect(getTimeState(false, true)).toEqual(['02', '41', 'PM']);
expect(getModelState(true)).toEqual([14, 41]);
expect(minutesEl.parent().hasClass('has-error')).toBe(false);
expect(element.hasClass('ng-invalid-time')).toBe(false);
});
});
describe('custom template and controllerAs', function() {
it('should allow custom templates', function() {
$templateCache.put('foo/bar.html', '
baz
');
element = $compile('
')($rootScope);
$rootScope.$digest();
expect(element[0].tagName.toLowerCase()).toBe('div');
expect(element.html()).toBe('
baz
');
});
it('should expose the controller on the view', function() {
$templateCache.put('uib/template/timepicker/timepicker.html', '
');
element = $compile('
')($rootScope);
$rootScope.$digest();
var ctrl = element.controller('uibTimepicker');
expect(ctrl).toBeDefined();
ctrl.text = 'foo';
$rootScope.$digest();
expect(element.html()).toBe('
');
});
});
describe('ngDisabled', function() {
it('prevents modifying date via controls when true', function() {
$rootScope.disabled = false;
element = $compile('
')($rootScope);
$rootScope.$digest();
var inputs = element.find('input');
var hoursEl = inputs.eq(0), minutesEl = inputs.eq(1),
secondsEl = inputs.eq(2);
var upKeydownEvent = keydown('up');
var downKeydownEvent = keydown('down');
var leftKeydownEvent = keydown('left');
var upMouseWheelEvent = wheelThatMouse(1);
var downMouseWheelEvent = wheelThatMouse(-1);
expect(getTimeState()).toEqual(['02', '40', '25', 'PM']);
expect(getModelState()).toEqual([14, 40, 25]);
$rootScope.disabled = true;
$rootScope.$digest();
// UP
hoursEl.trigger(upKeydownEvent);
$rootScope.$digest();
expect(getTimeState()).toEqual(['02', '40', '25', 'PM']);
expect(getModelState()).toEqual([14, 40, 25]);
minutesEl.trigger(upKeydownEvent);
$rootScope.$digest();
expect(getTimeState()).toEqual(['02', '40', '25', 'PM']);
expect(getModelState()).toEqual([14, 40, 25]);
secondsEl.trigger(upKeydownEvent);
$rootScope.$digest();
expect(getTimeState()).toEqual(['02', '40', '25', 'PM']);
expect(getModelState()).toEqual([14, 40, 25]);
// DOWN
secondsEl.trigger(downKeydownEvent);
$rootScope.$digest();
expect(getTimeState()).toEqual(['02', '40', '25', 'PM']);
expect(getModelState()).toEqual([14, 40, 25]);
minutesEl.trigger(downKeydownEvent);
$rootScope.$digest();
expect(getTimeState()).toEqual(['02', '40', '25', 'PM']);
expect(getModelState()).toEqual([14, 40, 25]);
hoursEl.trigger(downKeydownEvent);
$rootScope.$digest();
expect(getTimeState()).toEqual(['02', '40', '25', 'PM']);
expect(getModelState()).toEqual([14, 40, 25]);
// Other keydown
hoursEl.trigger(leftKeydownEvent);
$rootScope.$digest();
expect(getTimeState()).toEqual(['02', '40', '25', 'PM']);
expect(getModelState()).toEqual([14, 40, 25]);
minutesEl.trigger(leftKeydownEvent);
$rootScope.$digest();
expect(getTimeState()).toEqual(['02', '40', '25', 'PM']);
expect(getModelState()).toEqual([14, 40, 25]);
secondsEl.trigger(leftKeydownEvent);
$rootScope.$digest();
expect(getTimeState()).toEqual(['02', '40', '25', 'PM']);
expect(getModelState()).toEqual([14, 40, 25]);
// WHEEL UP
secondsEl.trigger(upMouseWheelEvent);
$rootScope.$digest();
expect(getTimeState()).toEqual(['02', '40', '25', 'PM']);
expect(getModelState()).toEqual([14, 40, 25]);
minutesEl.trigger(upMouseWheelEvent);
$rootScope.$digest();
expect(getTimeState()).toEqual(['02', '40', '25', 'PM']);
expect(getModelState()).toEqual([14, 40, 25]);
hoursEl.trigger(upMouseWheelEvent);
$rootScope.$digest();
expect(getTimeState()).toEqual(['02', '40', '25', 'PM']);
expect(getModelState()).toEqual([14, 40, 25]);
// WHEEL DOWN
secondsEl.trigger(downMouseWheelEvent);
$rootScope.$digest();
expect(getTimeState()).toEqual(['02', '40', '25', 'PM']);
expect(getModelState()).toEqual([14, 40, 25]);
minutesEl.trigger(downMouseWheelEvent);
$rootScope.$digest();
expect(getTimeState()).toEqual(['02', '40', '25', 'PM']);
expect(getModelState()).toEqual([14, 40, 25]);
hoursEl.trigger(downMouseWheelEvent);
$rootScope.$digest();
expect(getTimeState()).toEqual(['02', '40', '25', 'PM']);
expect(getModelState()).toEqual([14, 40, 25]);
});
});
describe('gc', function() {
var $scope;
beforeEach(inject(function() {
$scope = $rootScope.$new();
element = $compile('
')($scope);
$rootScope.$digest();
}));
it('should clean up watchers', function() {
expect($scope.$$watchers.length > 1).toBe(true);
element.isolateScope().$destroy();
expect($scope.$$watchers.length).toBe(1);
});
});
});
================================================
FILE: src/timepicker/timepicker.css
================================================
.uib-time input {
width: 50px;
}
================================================
FILE: src/timepicker/timepicker.js
================================================
angular.module('ui.bootstrap.timepicker', [])
.constant('uibTimepickerConfig', {
hourStep: 1,
minuteStep: 1,
secondStep: 1,
showMeridian: true,
showSeconds: false,
meridians: null,
readonlyInput: false,
mousewheel: true,
arrowkeys: true,
showSpinners: true,
templateUrl: 'uib/template/timepicker/timepicker.html'
})
.controller('UibTimepickerController', ['$scope', '$element', '$attrs', '$parse', '$log', '$locale', 'uibTimepickerConfig', function($scope, $element, $attrs, $parse, $log, $locale, timepickerConfig) {
var hoursModelCtrl, minutesModelCtrl, secondsModelCtrl;
var selected = new Date(),
watchers = [],
ngModelCtrl = { $setViewValue: angular.noop }, // nullModelCtrl
meridians = angular.isDefined($attrs.meridians) ? $scope.$parent.$eval($attrs.meridians) : timepickerConfig.meridians || $locale.DATETIME_FORMATS.AMPMS,
padHours = angular.isDefined($attrs.padHours) ? $scope.$parent.$eval($attrs.padHours) : true;
$scope.tabindex = angular.isDefined($attrs.tabindex) ? $attrs.tabindex : 0;
$element.removeAttr('tabindex');
this.init = function(ngModelCtrl_, inputs) {
ngModelCtrl = ngModelCtrl_;
ngModelCtrl.$render = this.render;
ngModelCtrl.$formatters.unshift(function(modelValue) {
return modelValue ? new Date(modelValue) : null;
});
var hoursInputEl = inputs.eq(0),
minutesInputEl = inputs.eq(1),
secondsInputEl = inputs.eq(2);
hoursModelCtrl = hoursInputEl.controller('ngModel');
minutesModelCtrl = minutesInputEl.controller('ngModel');
secondsModelCtrl = secondsInputEl.controller('ngModel');
var mousewheel = angular.isDefined($attrs.mousewheel) ? $scope.$parent.$eval($attrs.mousewheel) : timepickerConfig.mousewheel;
if (mousewheel) {
this.setupMousewheelEvents(hoursInputEl, minutesInputEl, secondsInputEl);
}
var arrowkeys = angular.isDefined($attrs.arrowkeys) ? $scope.$parent.$eval($attrs.arrowkeys) : timepickerConfig.arrowkeys;
if (arrowkeys) {
this.setupArrowkeyEvents(hoursInputEl, minutesInputEl, secondsInputEl);
}
$scope.readonlyInput = angular.isDefined($attrs.readonlyInput) ? $scope.$parent.$eval($attrs.readonlyInput) : timepickerConfig.readonlyInput;
this.setupInputEvents(hoursInputEl, minutesInputEl, secondsInputEl);
};
var hourStep = timepickerConfig.hourStep;
if ($attrs.hourStep) {
watchers.push($scope.$parent.$watch($parse($attrs.hourStep), function(value) {
hourStep = +value;
}));
}
var minuteStep = timepickerConfig.minuteStep;
if ($attrs.minuteStep) {
watchers.push($scope.$parent.$watch($parse($attrs.minuteStep), function(value) {
minuteStep = +value;
}));
}
var min;
watchers.push($scope.$parent.$watch($parse($attrs.min), function(value) {
var dt = new Date(value);
min = isNaN(dt) ? undefined : dt;
}));
var max;
watchers.push($scope.$parent.$watch($parse($attrs.max), function(value) {
var dt = new Date(value);
max = isNaN(dt) ? undefined : dt;
}));
var disabled = false;
if ($attrs.ngDisabled) {
watchers.push($scope.$parent.$watch($parse($attrs.ngDisabled), function(value) {
disabled = value;
}));
}
$scope.noIncrementHours = function() {
var incrementedSelected = addMinutes(selected, hourStep * 60);
return disabled || incrementedSelected > max ||
incrementedSelected < selected && incrementedSelected < min;
};
$scope.noDecrementHours = function() {
var decrementedSelected = addMinutes(selected, -hourStep * 60);
return disabled || decrementedSelected < min ||
decrementedSelected > selected && decrementedSelected > max;
};
$scope.noIncrementMinutes = function() {
var incrementedSelected = addMinutes(selected, minuteStep);
return disabled || incrementedSelected > max ||
incrementedSelected < selected && incrementedSelected < min;
};
$scope.noDecrementMinutes = function() {
var decrementedSelected = addMinutes(selected, -minuteStep);
return disabled || decrementedSelected < min ||
decrementedSelected > selected && decrementedSelected > max;
};
$scope.noIncrementSeconds = function() {
var incrementedSelected = addSeconds(selected, secondStep);
return disabled || incrementedSelected > max ||
incrementedSelected < selected && incrementedSelected < min;
};
$scope.noDecrementSeconds = function() {
var decrementedSelected = addSeconds(selected, -secondStep);
return disabled || decrementedSelected < min ||
decrementedSelected > selected && decrementedSelected > max;
};
$scope.noToggleMeridian = function() {
if (selected.getHours() < 12) {
return disabled || addMinutes(selected, 12 * 60) > max;
}
return disabled || addMinutes(selected, -12 * 60) < min;
};
var secondStep = timepickerConfig.secondStep;
if ($attrs.secondStep) {
watchers.push($scope.$parent.$watch($parse($attrs.secondStep), function(value) {
secondStep = +value;
}));
}
$scope.showSeconds = timepickerConfig.showSeconds;
if ($attrs.showSeconds) {
watchers.push($scope.$parent.$watch($parse($attrs.showSeconds), function(value) {
$scope.showSeconds = !!value;
}));
}
// 12H / 24H mode
$scope.showMeridian = timepickerConfig.showMeridian;
if ($attrs.showMeridian) {
watchers.push($scope.$parent.$watch($parse($attrs.showMeridian), function(value) {
$scope.showMeridian = !!value;
if (ngModelCtrl.$error.time) {
// Evaluate from template
var hours = getHoursFromTemplate(), minutes = getMinutesFromTemplate();
if (angular.isDefined(hours) && angular.isDefined(minutes)) {
selected.setHours(hours);
refresh();
}
} else {
updateTemplate();
}
}));
}
// Get $scope.hours in 24H mode if valid
function getHoursFromTemplate() {
var hours = +$scope.hours;
var valid = $scope.showMeridian ? hours > 0 && hours < 13 :
hours >= 0 && hours < 24;
if (!valid || $scope.hours === '') {
return undefined;
}
if ($scope.showMeridian) {
if (hours === 12) {
hours = 0;
}
if ($scope.meridian === meridians[1]) {
hours = hours + 12;
}
}
return hours;
}
function getMinutesFromTemplate() {
var minutes = +$scope.minutes;
var valid = minutes >= 0 && minutes < 60;
if (!valid || $scope.minutes === '') {
return undefined;
}
return minutes;
}
function getSecondsFromTemplate() {
var seconds = +$scope.seconds;
return seconds >= 0 && seconds < 60 ? seconds : undefined;
}
function pad(value, noPad) {
if (value === null) {
return '';
}
return angular.isDefined(value) && value.toString().length < 2 && !noPad ?
'0' + value : value.toString();
}
// Respond on mousewheel spin
this.setupMousewheelEvents = function(hoursInputEl, minutesInputEl, secondsInputEl) {
var isScrollingUp = function(e) {
if (e.originalEvent) {
e = e.originalEvent;
}
//pick correct delta variable depending on event
var delta = e.wheelDelta ? e.wheelDelta : -e.deltaY;
return e.detail || delta > 0;
};
hoursInputEl.on('mousewheel wheel', function(e) {
if (!disabled) {
$scope.$apply(isScrollingUp(e) ? $scope.incrementHours() : $scope.decrementHours());
}
e.preventDefault();
});
minutesInputEl.on('mousewheel wheel', function(e) {
if (!disabled) {
$scope.$apply(isScrollingUp(e) ? $scope.incrementMinutes() : $scope.decrementMinutes());
}
e.preventDefault();
});
secondsInputEl.on('mousewheel wheel', function(e) {
if (!disabled) {
$scope.$apply(isScrollingUp(e) ? $scope.incrementSeconds() : $scope.decrementSeconds());
}
e.preventDefault();
});
};
// Respond on up/down arrowkeys
this.setupArrowkeyEvents = function(hoursInputEl, minutesInputEl, secondsInputEl) {
hoursInputEl.on('keydown', function(e) {
if (!disabled) {
if (e.which === 38) { // up
e.preventDefault();
$scope.incrementHours();
$scope.$apply();
} else if (e.which === 40) { // down
e.preventDefault();
$scope.decrementHours();
$scope.$apply();
}
}
});
minutesInputEl.on('keydown', function(e) {
if (!disabled) {
if (e.which === 38) { // up
e.preventDefault();
$scope.incrementMinutes();
$scope.$apply();
} else if (e.which === 40) { // down
e.preventDefault();
$scope.decrementMinutes();
$scope.$apply();
}
}
});
secondsInputEl.on('keydown', function(e) {
if (!disabled) {
if (e.which === 38) { // up
e.preventDefault();
$scope.incrementSeconds();
$scope.$apply();
} else if (e.which === 40) { // down
e.preventDefault();
$scope.decrementSeconds();
$scope.$apply();
}
}
});
};
this.setupInputEvents = function(hoursInputEl, minutesInputEl, secondsInputEl) {
if ($scope.readonlyInput) {
$scope.updateHours = angular.noop;
$scope.updateMinutes = angular.noop;
$scope.updateSeconds = angular.noop;
return;
}
var invalidate = function(invalidHours, invalidMinutes, invalidSeconds) {
ngModelCtrl.$setViewValue(null);
ngModelCtrl.$setValidity('time', false);
if (angular.isDefined(invalidHours)) {
$scope.invalidHours = invalidHours;
if (hoursModelCtrl) {
hoursModelCtrl.$setValidity('hours', false);
}
}
if (angular.isDefined(invalidMinutes)) {
$scope.invalidMinutes = invalidMinutes;
if (minutesModelCtrl) {
minutesModelCtrl.$setValidity('minutes', false);
}
}
if (angular.isDefined(invalidSeconds)) {
$scope.invalidSeconds = invalidSeconds;
if (secondsModelCtrl) {
secondsModelCtrl.$setValidity('seconds', false);
}
}
};
$scope.updateHours = function() {
var hours = getHoursFromTemplate(),
minutes = getMinutesFromTemplate();
ngModelCtrl.$setDirty();
if (angular.isDefined(hours) && angular.isDefined(minutes)) {
selected.setHours(hours);
selected.setMinutes(minutes);
if (selected < min || selected > max) {
invalidate(true);
} else {
refresh('h');
}
} else {
invalidate(true);
}
};
hoursInputEl.on('blur', function(e) {
ngModelCtrl.$setTouched();
if (modelIsEmpty()) {
makeValid();
} else if ($scope.hours === null || $scope.hours === '') {
invalidate(true);
} else if (!$scope.invalidHours && $scope.hours < 10) {
$scope.$apply(function() {
$scope.hours = pad($scope.hours, !padHours);
});
}
});
$scope.updateMinutes = function() {
var minutes = getMinutesFromTemplate(),
hours = getHoursFromTemplate();
ngModelCtrl.$setDirty();
if (angular.isDefined(minutes) && angular.isDefined(hours)) {
selected.setHours(hours);
selected.setMinutes(minutes);
if (selected < min || selected > max) {
invalidate(undefined, true);
} else {
refresh('m');
}
} else {
invalidate(undefined, true);
}
};
minutesInputEl.on('blur', function(e) {
ngModelCtrl.$setTouched();
if (modelIsEmpty()) {
makeValid();
} else if ($scope.minutes === null) {
invalidate(undefined, true);
} else if (!$scope.invalidMinutes && $scope.minutes < 10) {
$scope.$apply(function() {
$scope.minutes = pad($scope.minutes);
});
}
});
$scope.updateSeconds = function() {
var seconds = getSecondsFromTemplate();
ngModelCtrl.$setDirty();
if (angular.isDefined(seconds)) {
selected.setSeconds(seconds);
refresh('s');
} else {
invalidate(undefined, undefined, true);
}
};
secondsInputEl.on('blur', function(e) {
if (modelIsEmpty()) {
makeValid();
} else if (!$scope.invalidSeconds && $scope.seconds < 10) {
$scope.$apply( function() {
$scope.seconds = pad($scope.seconds);
});
}
});
};
this.render = function() {
var date = ngModelCtrl.$viewValue;
if (isNaN(date)) {
ngModelCtrl.$setValidity('time', false);
$log.error('Timepicker directive: "ng-model" value must be a Date object, a number of milliseconds since 01.01.1970 or a string representing an RFC2822 or ISO 8601 date.');
} else {
if (date) {
selected = date;
}
if (selected < min || selected > max) {
ngModelCtrl.$setValidity('time', false);
$scope.invalidHours = true;
$scope.invalidMinutes = true;
} else {
makeValid();
}
updateTemplate();
}
};
// Call internally when we know that model is valid.
function refresh(keyboardChange) {
makeValid();
ngModelCtrl.$setViewValue(new Date(selected));
updateTemplate(keyboardChange);
}
function makeValid() {
if (hoursModelCtrl) {
hoursModelCtrl.$setValidity('hours', true);
}
if (minutesModelCtrl) {
minutesModelCtrl.$setValidity('minutes', true);
}
if (secondsModelCtrl) {
secondsModelCtrl.$setValidity('seconds', true);
}
ngModelCtrl.$setValidity('time', true);
$scope.invalidHours = false;
$scope.invalidMinutes = false;
$scope.invalidSeconds = false;
}
function updateTemplate(keyboardChange) {
if (!ngModelCtrl.$modelValue) {
$scope.hours = null;
$scope.minutes = null;
$scope.seconds = null;
$scope.meridian = meridians[0];
} else {
var hours = selected.getHours(),
minutes = selected.getMinutes(),
seconds = selected.getSeconds();
if ($scope.showMeridian) {
hours = hours === 0 || hours === 12 ? 12 : hours % 12; // Convert 24 to 12 hour system
}
$scope.hours = keyboardChange === 'h' ? hours : pad(hours, !padHours);
if (keyboardChange !== 'm') {
$scope.minutes = pad(minutes);
}
$scope.meridian = selected.getHours() < 12 ? meridians[0] : meridians[1];
if (keyboardChange !== 's') {
$scope.seconds = pad(seconds);
}
$scope.meridian = selected.getHours() < 12 ? meridians[0] : meridians[1];
}
}
function addSecondsToSelected(seconds) {
selected = addSeconds(selected, seconds);
refresh();
}
function addMinutes(selected, minutes) {
return addSeconds(selected, minutes*60);
}
function addSeconds(date, seconds) {
var dt = new Date(date.getTime() + seconds * 1000);
var newDate = new Date(date);
newDate.setHours(dt.getHours(), dt.getMinutes(), dt.getSeconds());
return newDate;
}
function modelIsEmpty() {
return ($scope.hours === null || $scope.hours === '') &&
($scope.minutes === null || $scope.minutes === '') &&
(!$scope.showSeconds || $scope.showSeconds && ($scope.seconds === null || $scope.seconds === ''));
}
$scope.showSpinners = angular.isDefined($attrs.showSpinners) ?
$scope.$parent.$eval($attrs.showSpinners) : timepickerConfig.showSpinners;
$scope.incrementHours = function() {
if (!$scope.noIncrementHours()) {
addSecondsToSelected(hourStep * 60 * 60);
}
};
$scope.decrementHours = function() {
if (!$scope.noDecrementHours()) {
addSecondsToSelected(-hourStep * 60 * 60);
}
};
$scope.incrementMinutes = function() {
if (!$scope.noIncrementMinutes()) {
addSecondsToSelected(minuteStep * 60);
}
};
$scope.decrementMinutes = function() {
if (!$scope.noDecrementMinutes()) {
addSecondsToSelected(-minuteStep * 60);
}
};
$scope.incrementSeconds = function() {
if (!$scope.noIncrementSeconds()) {
addSecondsToSelected(secondStep);
}
};
$scope.decrementSeconds = function() {
if (!$scope.noDecrementSeconds()) {
addSecondsToSelected(-secondStep);
}
};
$scope.toggleMeridian = function() {
var minutes = getMinutesFromTemplate(),
hours = getHoursFromTemplate();
if (!$scope.noToggleMeridian()) {
if (angular.isDefined(minutes) && angular.isDefined(hours)) {
addSecondsToSelected(12 * 60 * (selected.getHours() < 12 ? 60 : -60));
} else {
$scope.meridian = $scope.meridian === meridians[0] ? meridians[1] : meridians[0];
}
}
};
$scope.blur = function() {
ngModelCtrl.$setTouched();
};
$scope.$on('$destroy', function() {
while (watchers.length) {
watchers.shift()();
}
});
}])
.directive('uibTimepicker', ['uibTimepickerConfig', function(uibTimepickerConfig) {
return {
require: ['uibTimepicker', '?^ngModel'],
restrict: 'A',
controller: 'UibTimepickerController',
controllerAs: 'timepicker',
scope: {},
templateUrl: function(element, attrs) {
return attrs.templateUrl || uibTimepickerConfig.templateUrl;
},
link: function(scope, element, attrs, ctrls) {
var timepickerCtrl = ctrls[0], ngModelCtrl = ctrls[1];
if (ngModelCtrl) {
timepickerCtrl.init(ngModelCtrl, element.find('input'));
}
}
};
}]);
================================================
FILE: src/tooltip/docs/demo.html
================================================
================================================
FILE: src/tooltip/docs/demo.js
================================================
angular.module('ui.bootstrap.demo').controller('TooltipDemoCtrl', function ($scope, $sce) {
$scope.dynamicTooltip = 'Hello, World!';
$scope.dynamicTooltipText = 'dynamic';
$scope.htmlTooltip = $sce.trustAsHtml('I\'ve been made
bold!');
$scope.placement = {
options: [
'top',
'top-left',
'top-right',
'bottom',
'bottom-left',
'bottom-right',
'left',
'left-top',
'left-bottom',
'right',
'right-top',
'right-bottom'
],
selected: 'top'
};
});
================================================
FILE: src/tooltip/docs/readme.md
================================================
A lightweight, extensible directive for fancy tooltip creation. The tooltip
directive supports multiple placements, optional transition animation, and more.
__Note to mobile developers__: Please note that while tooltips may work correctly on mobile devices (including tablets),
we have made the decision to not officially support such a use-case because it does not make sense from a UX perspective.
There are three versions of the tooltip: `uib-tooltip`, `uib-tooltip-template`, and
`uib-tooltip-html`:
* `uib-tooltip` -
Takes text only and will escape any HTML provided.
* `uib-tooltip-html`
$ -
Takes an expression that evaluates to an HTML string. Note that this HTML is not compiled. If compilation is required, please use the `uib-tooltip-template` attribute option instead. *The user is responsible for ensuring the content is safe to put into the DOM!*
* `uib-tooltip-template`
$ -
Takes text that specifies the location of a template to use for the tooltip. Note that this needs to be wrapped in a tag.
### uib-tooltip-* settings
All these settings are available for the three types of tooltips.
* `tooltip-animation`
$
C
_(Default: `true`, Config: `animation`)_ -
Should it fade in and out?
* `tooltip-append-to-body`
$
C
_(Default: `false`, Config: `appendToBody`)_ -
Should the tooltip be appended to '$body' instead of the parent element?
* `tooltip-class` -
Custom class to be applied to the tooltip.
* `tooltip-enable`
$
_(Default: `true`)_ -
Is it enabled? It will enable or disable the configured tooltip-trigger.
* `tooltip-is-open`
_(Default: `false`)_ -
Whether to show the tooltip.
* `tooltip-placement`
C
_(Default: `top`, Config: `placement`)_ -
Passing in 'auto' separated by a space before the placement will enable auto positioning, e.g: "auto bottom-left". The tooltip will attempt to position where it fits in the closest scrollable ancestor. Accepts:
* `top` - tooltip on top, horizontally centered on host element.
* `top-left` - tooltip on top, left edge aligned with host element left edge.
* `top-right` - tooltip on top, right edge aligned with host element right edge.
* `bottom` - tooltip on bottom, horizontally centered on host element.
* `bottom-left` - tooltip on bottom, left edge aligned with host element left edge.
* `bottom-right` - tooltip on bottom, right edge aligned with host element right edge.
* `left` - tooltip on left, vertically centered on host element.
* `left-top` - tooltip on left, top edge aligned with host element top edge.
* `left-bottom` - tooltip on left, bottom edge aligned with host element bottom edge.
* `right` - tooltip on right, vertically centered on host element.
* `right-top` - tooltip on right, top edge aligned with host element top edge.
* `right-bottom` - tooltip on right, bottom edge aligned with host element bottom edge.
* `tooltip-popup-close-delay`
C
_(Default: `0`, Config: `popupCloseDelay`)_ -
For how long should the tooltip remain open after the close trigger event?
* `tooltip-popup-delay`
C
_(Default: `0`, Config: `popupDelay`)_ -
Popup delay in milliseconds until it opens.
* `tooltip-trigger`
$
_(Default: `'mouseenter'`)_ -
What should trigger a show of the tooltip? Supports a space separated list of event names, or objects (see below).
**Note:** To configure the tooltips, you need to do it on `$uibTooltipProvider` (also see below).
### Triggers
The following show triggers are supported out of the box, along with their provided hide triggers:
- `mouseenter`: `mouseleave`
- `click`: `click`
- `outsideClick`: `outsideClick`
- `focus`: `blur`
- `none`
The `outsideClick` trigger will cause the tooltip to toggle on click, and hide when anything else is clicked.
For any non-supported value, the trigger will be used to both show and hide the
tooltip. Using the 'none' trigger will disable the internal trigger(s), one can
then use the `tooltip-is-open` attribute exclusively to show and hide the tooltip.
### $uibTooltipProvider
Through the `$uibTooltipProvider`, you can change the way tooltips and popovers
behave by default; the attributes above always take precedence. The following
methods are available:
* `setTriggers(obj)`
_(Example: `{ 'openTrigger': 'closeTrigger' }`)_ -
Extends the default trigger mappings mentioned above with mappings of your own.
* `options(obj)` -
Provide a set of defaults for certain tooltip and popover attributes. Currently supports the ones with the
C badge.
### Known issues
For Safari 7+ support, if you want to use the **focus** `tooltip-trigger`, you need to use an anchor tag with a tab index. For example:
```
Click Me
```
For Safari (potentially all versions up to 9), there is an issue with the hover CSS selector when using multiple elements grouped close to each other that are using the tooltip - it is possible for multiple elements to gain the hover state when mousing between the elements quickly and exiting the container at the right time. See [issue #5445](https://github.com/angular-ui/bootstrap/issues/5445) for more details.
================================================
FILE: src/tooltip/index-nocss.js
================================================
require('../position/index-nocss.js');
require('../stackedMap');
require('../../template/tooltip/tooltip-popup.html.js');
require('../../template/tooltip/tooltip-html-popup.html.js');
require('../../template/tooltip/tooltip-template-popup.html.js');
require('./tooltip');
var MODULE_NAME = 'ui.bootstrap.module.tooltip';
angular.module(MODULE_NAME, ['ui.bootstrap.tooltip', 'uib/template/tooltip/tooltip-popup.html', 'uib/template/tooltip/tooltip-html-popup.html', 'uib/template/tooltip/tooltip-template-popup.html']);
module.exports = MODULE_NAME;
================================================
FILE: src/tooltip/index.js
================================================
require('../position/position.css');
require('./tooltip.css');
module.exports = require('./index-nocss.js');
================================================
FILE: src/tooltip/test/tooltip-template.spec.js
================================================
describe('tooltip template', function() {
var elm,
elmBody,
scope,
elmScope,
tooltipScope,
$document;
// load the popover code
beforeEach(module('ui.bootstrap.tooltip'));
// load the template
beforeEach(module('uib/template/tooltip/tooltip-template-popup.html'));
beforeEach(inject(function($templateCache) {
$templateCache.put('myUrl', [200, '
{{ myTemplateText }}', {}]);
}));
beforeEach(inject(function($rootScope, $compile, _$document_) {
$document = _$document_;
elmBody = angular.element(
'
Selector Text
'
);
scope = $rootScope;
$compile(elmBody)(scope);
scope.templateUrl = 'myUrl';
scope.$digest();
elm = elmBody.find('span');
elmScope = elm.scope();
tooltipScope = elmScope.$$childTail;
}));
afterEach(function() {
$document.off('keypress');
});
function trigger(element, evt) {
element.trigger(evt);
element.scope().$$childTail.$digest();
}
it('should open on mouseenter', inject(function() {
trigger(elm, 'mouseenter');
expect(tooltipScope.isOpen).toBe(true);
expect(elmBody.children().length).toBe(2);
}));
it('should not open on mouseenter if templateUrl is empty', inject(function() {
scope.templateUrl = null;
scope.$digest();
trigger(elm, 'mouseenter');
expect(tooltipScope.isOpen).toBe(false);
expect(elmBody.children().length).toBe(1);
}));
it('should show updated text', inject(function() {
scope.myTemplateText = 'some text';
trigger(elm, 'mouseenter');
expect(tooltipScope.isOpen).toBe(true);
scope.$digest();
expect(elmBody.children().eq(1).text().trim()).toBe('some text');
scope.myTemplateText = 'new text';
scope.$digest();
expect(elmBody.children().eq(1).text().trim()).toBe('new text');
}));
it('should hide tooltip when template becomes empty', inject(function($timeout) {
trigger(elm, 'mouseenter');
$timeout.flush(0);
expect(tooltipScope.isOpen).toBe(true);
scope.templateUrl = '';
scope.$digest();
expect(tooltipScope.isOpen).toBe(false);
$timeout.flush();
expect(elmBody.children().length).toBe(1);
}));
});
================================================
FILE: src/tooltip/test/tooltip.spec.js
================================================
describe('tooltip', function() {
var elm,
elmBody,
scope,
elmScope,
tooltipScope,
$document;
// load the tooltip code
beforeEach(module('ui.bootstrap.tooltip'));
// load the template
beforeEach(module('uib/template/tooltip/tooltip-popup.html'));
beforeEach(inject(function($rootScope, $compile, _$document_) {
elmBody = angular.element(
'
Selector Text
'
);
$document = _$document_;
scope = $rootScope;
$compile(elmBody)(scope);
scope.$digest();
elm = elmBody.find('span');
elmScope = elm.scope();
tooltipScope = elmScope.$$childTail;
}));
afterEach(function() {
$document.off('keyup');
});
function trigger(element, evt) {
element.trigger(evt);
element.scope().$$childTail.$digest();
}
it('should not be open initially', inject(function() {
expect(tooltipScope.isOpen).toBe(false);
// We can only test *that* the tooltip-popup element wasn't created as the
// implementation is templated and replaced.
expect(elmBody.children().length).toBe(1);
}));
it('should open on mouseenter', inject(function() {
trigger(elm, 'mouseenter');
expect(tooltipScope.isOpen).toBe(true);
// We can only test *that* the tooltip-popup element was created as the
// implementation is templated and replaced.
expect(elmBody.children().length).toBe(2);
}));
it('should close on mouseleave', inject(function() {
trigger(elm, 'mouseenter');
trigger(elm, 'mouseleave');
expect(tooltipScope.isOpen).toBe(false);
}));
it('should not animate on animation set to false', inject(function() {
expect(tooltipScope.animation).toBe(false);
}));
it('should have default placement of "top"', inject(function() {
trigger(elm, 'mouseenter');
expect(tooltipScope.placement).toBe('top');
}));
it('should allow specification of placement', inject(function($compile) {
elm = $compile(angular.element(
'
Selector Text'
))(scope);
scope.$apply();
elmScope = elm.scope();
tooltipScope = elmScope.$$childTail;
trigger(elm, 'mouseenter');
expect(tooltipScope.placement).toBe('bottom');
}));
it('should update placement dynamically', inject(function($compile, $timeout) {
scope.place = 'bottom';
elm = $compile(angular.element(
'
Selector Text'
))(scope);
scope.$apply();
elmScope = elm.scope();
tooltipScope = elmScope.$$childTail;
trigger(elm, 'mouseenter');
expect(tooltipScope.placement).toBe('bottom');
scope.place = 'right';
scope.$digest();
$timeout.flush();
expect(tooltipScope.placement).toBe('right');
}));
it('should work inside an ngRepeat', inject(function($compile) {
elm = $compile(angular.element(
'
'+
'- '+
'{{item.name}}'+
'
'+
'
'
))(scope);
scope.items = [
{ name: 'One', tooltip: 'First Tooltip' }
];
scope.$digest();
var tt = angular.element(elm.find('li > span')[0]);
trigger(tt, 'mouseenter');
expect(tt.text()).toBe(scope.items[0].name);
tooltipScope = tt.scope().$$childTail;
expect(tooltipScope.content).toBe(scope.items[0].tooltip);
trigger(tt, 'mouseleave');
expect(tooltipScope.isOpen).toBeFalsy();
}));
it('should show correct text when in an ngRepeat', inject(function($compile, $timeout) {
elm = $compile(angular.element(
'
'+
'- '+
'{{item.name}}'+
'
'+
'
'
))(scope);
scope.items = [
{ name: 'One', tooltip: 'First Tooltip' },
{ name: 'Second', tooltip: 'Second Tooltip' }
];
scope.$digest();
var tt_1 = angular.element(elm.find('li > span')[0]);
var tt_2 = angular.element(elm.find('li > span')[1]);
trigger(tt_1, 'mouseenter');
trigger(tt_1, 'mouseleave');
$timeout.flush();
trigger(tt_2, 'mouseenter');
expect(tt_1.text()).toBe(scope.items[0].name);
expect(tt_2.text()).toBe(scope.items[1].name);
tooltipScope = tt_2.scope().$$childTail;
expect(tooltipScope.content).toBe(scope.items[1].tooltip);
expect(elm.find('.tooltip-inner').text()).toBe(scope.items[1].tooltip);
trigger(tt_2, 'mouseleave');
}));
it('should only have an isolate scope on the popup', inject(function($compile) {
var ttScope;
scope.tooltipMsg = 'Tooltip Text';
scope.alt = 'Alt Message';
elmBody = $compile(angular.element(
'
Selector Text
'
))(scope);
$compile(elmBody)(scope);
scope.$digest();
elm = elmBody.find('span');
elmScope = elm.scope();
trigger(elm, 'mouseenter');
expect(elm.attr('alt')).toBe(scope.alt);
ttScope = angular.element(elmBody.children()[1]).isolateScope();
expect(ttScope.content).toBe(scope.tooltipMsg);
trigger(elm, 'mouseleave');
//Isolate scope contents should be the same after hiding and showing again (issue 1191)
trigger(elm, 'mouseenter');
ttScope = angular.element(elmBody.children()[1]).isolateScope();
expect(ttScope.content).toBe(scope.tooltipMsg);
}));
it('should not show tooltips if there is nothing to show - issue #129', inject(function($compile) {
elmBody = $compile(angular.element(
'
Selector Text
'
))(scope);
scope.$digest();
elmBody.find('span').trigger('mouseenter');
expect(elmBody.children().length).toBe(1);
}));
it('should close the tooltip when its trigger element is destroyed', inject(function() {
trigger(elm, 'mouseenter');
expect(tooltipScope.isOpen).toBe(true);
elm.remove();
elmScope.$destroy();
expect(elmBody.children().length).toBe(0);
}));
it('issue 1191 - scope on the popup should always be child of correct element scope', function() {
var ttScope;
trigger(elm, 'mouseenter');
ttScope = angular.element(elmBody.children()[1]).scope();
expect(ttScope.$parent).toBe(tooltipScope);
trigger(elm, 'mouseleave');
// After leaving and coming back, the scope's parent should be the same
trigger(elm, 'mouseenter');
ttScope = angular.element(elmBody.children()[1]).scope();
expect(ttScope.$parent).toBe(tooltipScope);
trigger(elm, 'mouseleave');
});
describe('with specified enable expression', function() {
beforeEach(inject(function($compile) {
scope.enable = false;
elmBody = $compile(angular.element(
'
Selector Text
'
))(scope);
scope.$digest();
elm = elmBody.find('span');
elmScope = elm.scope();
tooltipScope = elmScope.$$childTail;
}));
it('should not open ', inject(function() {
trigger(elm, 'mouseenter');
expect(tooltipScope.isOpen).toBeFalsy();
expect(elmBody.children().length).toBe(1);
}));
it('should open', inject(function() {
scope.enable = true;
scope.$digest();
trigger(elm, 'mouseenter');
expect(tooltipScope.isOpen).toBeTruthy();
expect(elmBody.children().length).toBe(2);
}));
});
describe('with specified popup delay', function() {
var $timeout;
beforeEach(inject(function($compile, _$timeout_) {
$timeout = _$timeout_;
scope.delay = '1000';
elm = $compile(angular.element(
'
Selector Text'
))(scope);
elmScope = elm.scope();
tooltipScope = elmScope.$$childTail;
scope.$digest();
}));
it('should open after timeout', function() {
trigger(elm, 'mouseenter');
expect(tooltipScope.isOpen).toBe(false);
$timeout.flush();
expect(tooltipScope.isOpen).toBe(true);
});
it('should not open if mouseleave before timeout', function() {
trigger(elm, 'mouseenter');
expect(tooltipScope.isOpen).toBe(false);
trigger(elm, 'mouseleave');
$timeout.flush();
expect(tooltipScope.isOpen).toBe(false);
});
it('should use default popup delay if specified delay is not a number', function() {
scope.delay = 'text1000';
scope.$digest();
trigger(elm, 'mouseenter');
expect(tooltipScope.isOpen).toBe(true);
});
it('should not open if disabled is present', function() {
trigger(elm, 'mouseenter');
expect(tooltipScope.isOpen).toBe(false);
elmScope.disabled = true;
elmScope.$digest();
$timeout.flush(500);
expect(tooltipScope.isOpen).toBe(false);
});
it('should open when not disabled after being disabled - issue #4204', function() {
trigger(elm, 'mouseenter');
expect(tooltipScope.isOpen).toBe(false);
$timeout.flush(500);
elmScope.disabled = true;
elmScope.$digest();
$timeout.flush(500);
expect(tooltipScope.isOpen).toBe(false);
elmScope.disabled = false;
elmScope.$digest();
trigger(elm, 'mouseenter');
$timeout.flush();
expect(tooltipScope.isOpen).toBe(true);
});
it('should close the tooltips in order', inject(function($compile) {
var elm2 = $compile('
Selector Text
')(scope);
scope.$digest();
elm2 = elm2.find('span');
var tooltipScope2 = elm2.scope().$$childTail;
tooltipScope2.isOpen = false;
scope.$digest();
trigger(elm, 'mouseenter');
tooltipScope2.$digest();
$timeout.flush();
expect(tooltipScope.isOpen).toBe(true);
expect(tooltipScope2.isOpen).toBe(false);
trigger(elm2, 'mouseenter');
tooltipScope2.$digest();
$timeout.flush();
expect(tooltipScope.isOpen).toBe(true);
expect(tooltipScope2.isOpen).toBe(true);
var evt = $.Event('keyup');
evt.which = 27;
$document.trigger(evt);
tooltipScope.$digest();
tooltipScope2.$digest();
$timeout.flush();
expect(tooltipScope.isOpen).toBe(true);
expect(tooltipScope2.isOpen).toBe(false);
var evt2 = $.Event('keyup');
evt2.which = 27;
$document.trigger(evt2);
tooltipScope.$digest();
tooltipScope2.$digest();
$timeout.flush(500);
expect(tooltipScope.isOpen).toBe(false);
expect(tooltipScope2.isOpen).toBe(false);
}));
});
describe('with specified popup close delay', function() {
var $timeout;
beforeEach(inject(function($compile, _$timeout_) {
$timeout = _$timeout_;
scope.delay = '1000';
elm = $compile(angular.element(
'
Selector Text'
))(scope);
elmScope = elm.scope();
tooltipScope = elmScope.$$childTail;
scope.$digest();
}));
it('should close after timeout', function() {
trigger(elm, 'mouseenter');
expect(tooltipScope.isOpen).toBe(true);
trigger(elm, 'mouseleave');
$timeout.flush();
expect(tooltipScope.isOpen).toBe(false);
});
it('should use default popup close delay if specified delay is not a number and close immediately', function() {
scope.delay = 'text1000';
scope.$digest();
trigger(elm, 'mouseenter');
expect(tooltipScope.popupCloseDelay).toBe(0);
expect(tooltipScope.isOpen).toBe(true);
trigger(elm, 'mouseleave');
$timeout.flush();
expect(tooltipScope.isOpen).toBe(false);
});
it('should open when not disabled after being disabled and close after delay - issue #4204', function() {
trigger(elm, 'mouseenter');
expect(tooltipScope.isOpen).toBe(true);
elmScope.disabled = true;
elmScope.$digest();
$timeout.flush(500);
expect(tooltipScope.isOpen).toBe(false);
elmScope.disabled = false;
elmScope.$digest();
trigger(elm, 'mouseenter');
expect(tooltipScope.isOpen).toBe(true);
trigger(elm, 'mouseleave');
$timeout.flush();
expect(tooltipScope.isOpen).toBe(false);
});
});
describe('with specified popup and popup close delay', function() {
var $timeout;
beforeEach(inject(function($compile, _$timeout_) {
$timeout = _$timeout_;
scope.delay = '1000';
elm = $compile(angular.element(
'
Selector Text'
))(scope);
elmScope = elm.scope();
tooltipScope = elmScope.$$childTail;
scope.$digest();
}));
it('should not open if mouseleave before timeout', function() {
trigger(elm, 'mouseenter');
$timeout.flush(500);
trigger(elm, 'mouseleave');
$timeout.flush();
expect(tooltipScope.isOpen).toBe(false);
});
});
describe('with an is-open attribute', function() {
beforeEach(inject(function ($compile) {
scope.isOpen = false;
elm = $compile(angular.element(
'
Selector Text'
))(scope);
elmScope = elm.scope();
tooltipScope = elmScope.$$childTail;
scope.$digest();
}));
it('should show and hide with the controller value', function() {
expect(tooltipScope.isOpen).toBe(false);
elmScope.isOpen = true;
elmScope.$digest();
expect(tooltipScope.isOpen).toBe(true);
elmScope.isOpen = false;
elmScope.$digest();
expect(tooltipScope.isOpen).toBe(false);
});
it('should update the controller value', function() {
trigger(elm, 'mouseenter');
expect(elmScope.isOpen).toBe(true);
trigger(elm, 'mouseleave');
expect(elmScope.isOpen).toBe(false);
});
});
describe('with an is-open attribute expression', function() {
beforeEach(inject(function($compile) {
scope.isOpen = false;
elm = $compile(angular.element(
'
Selector Text'
))(scope);
elmScope = elm.scope();
tooltipScope = elmScope.$$childTail;
scope.$digest();
}));
it('should show and hide with the expression', function() {
expect(tooltipScope.isOpen).toBe(false);
elmScope.isOpen = true;
elmScope.$digest();
expect(tooltipScope.isOpen).toBe(true);
elmScope.isOpen = false;
elmScope.$digest();
expect(tooltipScope.isOpen).toBe(false);
});
});
describe('with a trigger attribute', function() {
var scope, elmBody, elm, elmScope;
beforeEach(inject(function($rootScope) {
scope = $rootScope;
}));
it('should use it to show but set the hide trigger based on the map for mapped triggers', inject(function($compile) {
elmBody = angular.element(
'
'
);
$compile(elmBody)(scope);
scope.$apply();
elm = elmBody.find('input');
elmScope = elm.scope();
tooltipScope = elmScope.$$childTail;
expect(tooltipScope.isOpen).toBeFalsy();
trigger(elm, 'focus');
expect(tooltipScope.isOpen).toBeTruthy();
trigger(elm, 'blur');
expect(tooltipScope.isOpen).toBeFalsy();
}));
it('should use it as both the show and hide triggers for unmapped triggers', inject(function($compile) {
elmBody = angular.element(
'
'
);
$compile(elmBody)(scope);
scope.$apply();
elm = elmBody.find('input');
elmScope = elm.scope();
tooltipScope = elmScope.$$childTail;
expect(tooltipScope.isOpen).toBeFalsy();
trigger(elm, 'fakeTriggerAttr');
expect(tooltipScope.isOpen).toBeTruthy();
trigger(elm, 'fakeTriggerAttr');
expect(tooltipScope.isOpen).toBeFalsy();
}));
it('should only set up triggers once', inject(function($compile) {
scope.test = true;
elmBody = angular.element(
'
' +
'' +
'' +
'
'
);
$compile(elmBody)(scope);
scope.$apply();
var elm1 = elmBody.find('input').eq(0);
var elm2 = elmBody.find('input').eq(1);
var elmScope1 = elm1.scope();
var elmScope2 = elm2.scope();
var tooltipScope2 = elmScope2.$$childTail;
scope.$apply('test = false');
// click trigger isn't set
elm2.click();
expect(tooltipScope2.isOpen).toBeFalsy();
// mouseenter trigger is still set
trigger(elm2, 'mouseenter');
expect(tooltipScope2.isOpen).toBeTruthy();
}));
it('should accept multiple triggers based on the map for mapped triggers', inject(function($compile) {
elmBody = angular.element(
'
'
);
$compile(elmBody)(scope);
scope.$apply();
elm = elmBody.find('input');
elmScope = elm.scope();
tooltipScope = elmScope.$$childTail;
expect(tooltipScope.isOpen).toBeFalsy();
trigger(elm, 'focus');
expect(tooltipScope.isOpen).toBeTruthy();
trigger(elm, 'blur');
expect(tooltipScope.isOpen).toBeFalsy();
trigger(elm, 'fakeTriggerAttr');
expect(tooltipScope.isOpen).toBeTruthy();
trigger(elm, 'fakeTriggerAttr');
expect(tooltipScope.isOpen).toBeFalsy();
}));
it('should not show when trigger is set to "none"', inject(function($compile) {
elmBody = angular.element(
'
'
);
$compile(elmBody)(scope);
scope.$apply();
elm = elmBody.find('input');
elmScope = elm.scope();
tooltipScope = elmScope.$$childTail;
expect(tooltipScope.isOpen).toBeFalsy();
elm.trigger('mouseenter');
expect(tooltipScope.isOpen).toBeFalsy();
}));
it('should toggle on click and hide when anything else is clicked when trigger is set to "outsideClick"', inject(function($compile, $document) {
elm = $compile(angular.element(
'
Selector Text'
))(scope);
scope.$apply();
elmScope = elm.scope();
tooltipScope = elmScope.$$childTail;
// start off
expect(tooltipScope.isOpen).toBeFalsy();
// toggle
trigger(elm, 'click');
expect(tooltipScope.isOpen).toBeTruthy();
trigger(elm, 'click');
expect(tooltipScope.isOpen).toBeFalsy();
// click on, outsideClick off
trigger(elm, 'click');
expect(tooltipScope.isOpen).toBeTruthy();
angular.element($document[0].body).trigger('click');
tooltipScope.$digest();
expect(tooltipScope.isOpen).toBeFalsy();
}));
it('should support objects', inject(function($compile) {
elmBody = angular.element(
'
'
);
$compile(elmBody)(scope);
scope.$apply();
elm = elmBody.find('input');
elmScope = elm.scope();
tooltipScope = elmScope.$$childTail;
expect(tooltipScope.isOpen).toBeFalsy();
trigger(elm, 'show');
expect(tooltipScope.isOpen).toBeTruthy();
trigger(elm, 'hide');
expect(tooltipScope.isOpen).toBeFalsy();
}));
});
describe('with an append-to-body attribute', function() {
var scope, elmBody, elm, elmScope, $body;
beforeEach(inject(function($rootScope) {
scope = $rootScope;
}));
afterEach(function() {
$body.find('.tooltip').remove();
});
it('should append to the body', inject(function($compile, $document) {
$body = $document.find('body');
elmBody = angular.element(
'
Selector Text
'
);
$compile(elmBody)(scope);
scope.$digest();
elm = elmBody.find('span');
elmScope = elm.scope();
tooltipScope = elmScope.$$childTail;
var bodyLength = $body.children().length;
trigger(elm, 'mouseenter');
expect(tooltipScope.isOpen).toBe(true);
expect(elmBody.children().length).toBe(1);
expect($body.children().length).toEqual(bodyLength + 1);
}));
});
describe('cleanup', function() {
var elmBody, elm, elmScope, tooltipScope;
function inCache() {
var match = false;
angular.forEach(angular.element.cache, function(item) {
if (item.data && item.data.$scope === tooltipScope) {
match = true;
}
});
return match;
}
beforeEach(inject(function($compile, $rootScope) {
elmBody = angular.element('
');
$compile(elmBody)($rootScope);
$rootScope.$apply();
elm = elmBody.find('input');
elmScope = elm.scope();
trigger(elm, 'fooTrigger');
tooltipScope = elmScope.$$childTail.$$childTail;
}));
it('should not contain a cached reference when not visible', inject(function($timeout) {
expect(inCache()).toBeTruthy();
elmScope.$destroy();
expect(inCache()).toBeFalsy();
}));
});
describe('observers', function() {
var elmBody, elm, elmScope, scope, tooltipScope;
beforeEach(inject(function($compile, $rootScope) {
scope = $rootScope;
scope.content = 'tooltip content';
scope.placement = 'top';
elmBody = angular.element('
');
$compile(elmBody)(scope);
scope.$apply();
elm = elmBody.find('input');
elmScope = elm.scope();
tooltipScope = elmScope.$$childTail;
}));
it('should be removed when tooltip hides', inject(function($timeout) {
expect(tooltipScope.content).toBe(undefined);
expect(tooltipScope.placement).toBe(undefined);
trigger(elm, 'mouseenter');
expect(tooltipScope.content).toBe('tooltip content');
expect(tooltipScope.placement).toBe('top');
scope.content = 'tooltip content updated';
scope.placement = 'bottom';
scope.$apply();
expect(tooltipScope.content).toBe('tooltip content updated');
expect(tooltipScope.placement).toBe('bottom');
trigger(elm, 'mouseleave');
$timeout.flush();
scope.content = 'tooltip content updated after close';
scope.placement = 'left';
scope.$apply();
expect(tooltipScope.content).toBe('tooltip content updated');
expect(tooltipScope.placement).toBe('bottom');
}));
});
});
describe('tooltipWithDifferentSymbols', function() {
var elmBody;
// load the tooltip code
beforeEach(module('ui.bootstrap.tooltip'));
// load the template
beforeEach(module('uib/template/tooltip/tooltip-popup.html'));
// configure interpolate provider to use [[ ]] instead of {{ }}
beforeEach(module(function($interpolateProvider) {
$interpolateProvider.startSymbol('[[');
$interpolateProvider.startSymbol(']]');
}));
function trigger(element, evt) {
element.trigger(evt);
element.scope().$$childTail.$digest();
}
it('should show the correct tooltip text', inject(function($compile, $rootScope) {
elmBody = angular.element(
'
'
);
$compile(elmBody)($rootScope);
$rootScope.$apply();
var elmInput = elmBody.find('input');
trigger(elmInput, 'focus');
expect(elmInput.next().find('div').next().html()).toBe('My tooltip');
}));
});
describe('tooltip positioning', function() {
var elm, elmBody, elmScope, tooltipScope, scope;
var $position;
// load the tooltip code
beforeEach(module('ui.bootstrap.tooltip', function($uibTooltipProvider) {
$uibTooltipProvider.options({ animation: false });
}));
// load the template
beforeEach(module('uib/template/tooltip/tooltip-popup.html'));
beforeEach(inject(function($rootScope, $compile, $uibPosition) {
$position = $uibPosition;
spyOn($position, 'positionElements').and.callThrough();
scope = $rootScope;
scope.text = 'Some Text';
elmBody = $compile(angular.element(
'
Selector Text
'
))(scope);
scope.$digest();
elm = elmBody.find('span');
elmScope = elm.scope();
tooltipScope = elmScope.$$childTail;
}));
function trigger(element, evt) {
element.trigger(evt);
element.scope().$$childTail.$digest();
}
it('should re-position when value changes', inject(function($timeout) {
trigger(elm, 'mouseenter');
scope.$digest();
$timeout.flush();
var startingPositionCalls = $position.positionElements.calls.count();
scope.text = 'New Text';
scope.$digest();
$timeout.flush();
expect(elm.attr('uib-tooltip')).toBe('New Text');
expect($position.positionElements.calls.count()).toEqual(startingPositionCalls + 1);
// Check that positionElements was called with elm
expect($position.positionElements.calls.argsFor(startingPositionCalls)[0][0])
.toBe(elm[0]);
scope.$digest();
$timeout.verifyNoPendingTasks();
expect($position.positionElements.calls.count()).toEqual(startingPositionCalls + 1);
expect($position.positionElements.calls.argsFor(startingPositionCalls)[0][0])
.toBe(elm[0]);
scope.$digest();
}));
});
describe('tooltipHtml', function() {
var elm, elmBody, elmScope, tooltipScope, scope;
// load the tooltip code
beforeEach(module('ui.bootstrap.tooltip', function($uibTooltipProvider) {
$uibTooltipProvider.options({ animation: false });
}));
// load the template
beforeEach(module('uib/template/tooltip/tooltip-html-popup.html'));
beforeEach(inject(function($rootScope, $compile, $sce) {
scope = $rootScope;
scope.html = 'I say:
Hello!';
scope.safeHtml = $sce.trustAsHtml(scope.html);
elmBody = $compile(angular.element(
'
Selector Text
'
))(scope);
scope.$digest();
elm = elmBody.find('span');
elmScope = elm.scope();
tooltipScope = elmScope.$$childTail;
}));
function trigger(element, evt) {
element.trigger(evt);
element.scope().$$childTail.$digest();
}
it('should render html properly', inject(function() {
trigger(elm, 'mouseenter');
expect(elmBody.find('.tooltip-inner').html()).toBe(scope.html);
}));
it('should not open if html is empty', function() {
scope.safeHtml = null;
scope.$digest();
trigger(elm, 'mouseenter');
expect(tooltipScope.isOpen).toBe(false);
});
it('should show on mouseenter and hide on mouseleave', inject(function($sce) {
expect(tooltipScope.isOpen).toBe(false);
trigger(elm, 'mouseenter');
expect(tooltipScope.isOpen).toBe(true);
expect(elmBody.children().length).toBe(2);
expect($sce.getTrustedHtml(tooltipScope.contentExp())).toEqual(scope.html);
trigger(elm, 'mouseleave');
expect(tooltipScope.isOpen).toBe(false);
expect(elmBody.children().length).toBe(1);
}));
});
describe('$uibTooltipProvider', function() {
var elm,
elmBody,
scope,
elmScope,
tooltipScope;
function trigger(element, evt) {
element.trigger(evt);
element.scope().$$childTail.$digest();
}
describe('popupDelay', function() {
beforeEach(module('ui.bootstrap.tooltip', function($uibTooltipProvider) {
$uibTooltipProvider.options({popupDelay: 1000});
}));
// load the template
beforeEach(module('uib/template/tooltip/tooltip-popup.html'));
beforeEach(inject(function($rootScope, $compile) {
elmBody = angular.element(
'
Selector Text
'
);
scope = $rootScope;
$compile(elmBody)(scope);
scope.$digest();
elm = elmBody.find('span');
elmScope = elm.scope();
tooltipScope = elmScope.$$childTail;
}));
it('should open after timeout', inject(function($timeout) {
trigger(elm, 'mouseenter');
expect(tooltipScope.isOpen).toBe(false);
$timeout.flush();
expect(tooltipScope.isOpen).toBe(true);
}));
});
describe('appendToBody', function() {
var $body;
beforeEach(module('uib/template/tooltip/tooltip-popup.html'));
beforeEach(module('ui.bootstrap.tooltip', function($uibTooltipProvider) {
$uibTooltipProvider.options({ appendToBody: true });
}));
afterEach(function() {
$body.find('.tooltip').remove();
});
it('should append to the body', inject(function($rootScope, $compile, $document) {
$body = $document.find('body');
elmBody = angular.element(
'
Selector Text
'
);
scope = $rootScope;
$compile(elmBody)(scope);
scope.$digest();
elm = elmBody.find('span');
elmScope = elm.scope();
tooltipScope = elmScope.$$childTail;
var bodyLength = $body.children().length;
trigger(elm, 'mouseenter');
expect(tooltipScope.isOpen).toBe(true);
expect(elmBody.children().length).toBe(1);
expect($body.children().length).toEqual(bodyLength + 1);
}));
it('should append to the body when only attribute present', inject(function($rootScope, $compile, $document) {
$body = $document.find('body');
elmBody = angular.element(
'
Selector Text
'
);
scope = $rootScope;
$compile(elmBody)(scope);
scope.$digest();
elm = elmBody.find('span');
elmScope = elm.scope();
tooltipScope = elmScope.$$childTail;
var bodyLength = $body.children().length;
trigger(elm, 'mouseenter');
expect(tooltipScope.isOpen).toBe(true);
expect(elmBody.children().length).toBe(1);
expect($body.children().length).toEqual(bodyLength + 1);
}));
it('should not append to the body when attribute value is false', inject(function($rootScope, $compile, $document) {
$body = $document.find('body');
elmBody = angular.element(
'
Selector Text
'
);
scope = $rootScope;
$compile(elmBody)(scope);
scope.$digest();
elm = elmBody.find('span');
elmScope = elm.scope();
tooltipScope = elmScope.$$childTail;
var bodyLength = $body.children().length;
trigger(elm, 'mouseenter');
expect(tooltipScope.isOpen).toBe(true);
expect(elmBody.children().length).toBe(2);
expect($body.children().length).toEqual(bodyLength);
}));
});
describe('triggers', function() {
describe('with a mapped value', function() {
beforeEach(module('ui.bootstrap.tooltip', function($uibTooltipProvider) {
$uibTooltipProvider.options({trigger: 'focus'});
}));
// load the template
beforeEach(module('uib/template/tooltip/tooltip-popup.html'));
it('should use the show trigger and the mapped value for the hide trigger', inject(function($rootScope, $compile) {
elmBody = angular.element(
'
'
);
scope = $rootScope;
$compile(elmBody)(scope);
scope.$digest();
elm = elmBody.find('input');
elmScope = elm.scope();
tooltipScope = elmScope.$$childTail;
expect(tooltipScope.isOpen).toBeFalsy();
trigger(elm, 'focus');
expect(tooltipScope.isOpen).toBeTruthy();
trigger(elm, 'blur');
expect(tooltipScope.isOpen).toBeFalsy();
}));
it('should override the show and hide triggers if there is an attribute', inject(function($rootScope, $compile) {
elmBody = angular.element(
'
'
);
scope = $rootScope;
$compile(elmBody)(scope);
scope.$digest();
elm = elmBody.find('input');
elmScope = elm.scope();
tooltipScope = elmScope.$$childTail;
expect(tooltipScope.isOpen).toBeFalsy();
trigger(elm, 'mouseenter');
expect(tooltipScope.isOpen).toBeTruthy();
trigger(elm, 'mouseleave');
expect(tooltipScope.isOpen).toBeFalsy();
}));
});
describe('with a custom mapped value', function() {
beforeEach(module('ui.bootstrap.tooltip', function($uibTooltipProvider) {
$uibTooltipProvider.setTriggers({ customOpenTrigger: 'foo bar' });
$uibTooltipProvider.options({trigger: 'customOpenTrigger'});
}));
// load the template
beforeEach(module('uib/template/tooltip/tooltip-popup.html'));
it('should use the show trigger and the mapped value for the hide trigger', inject(function($rootScope, $compile) {
elmBody = angular.element(
'
'
);
scope = $rootScope;
$compile(elmBody)(scope);
scope.$digest();
elm = elmBody.find('input');
elmScope = elm.scope();
tooltipScope = elmScope.$$childTail;
expect(tooltipScope.isOpen).toBeFalsy();
trigger(elm, 'customOpenTrigger');
expect(tooltipScope.isOpen).toBeTruthy();
trigger(elm, 'foo');
expect(tooltipScope.isOpen).toBeFalsy();
trigger(elm, 'customOpenTrigger');
expect(tooltipScope.isOpen).toBeTruthy();
trigger(elm, 'bar');
expect(tooltipScope.isOpen).toBeFalsy();
}));
});
describe('triggers without a mapped value', function() {
beforeEach(module('ui.bootstrap.tooltip', function($uibTooltipProvider) {
$uibTooltipProvider.options({trigger: 'fakeTrigger'});
}));
// load the template
beforeEach(module('uib/template/tooltip/tooltip-popup.html'));
it('should use the show trigger to hide', inject(function($rootScope, $compile) {
elmBody = angular.element(
'
Selector Text
'
);
scope = $rootScope;
$compile(elmBody)(scope);
scope.$digest();
elm = elmBody.find('span');
elmScope = elm.scope();
tooltipScope = elmScope.$$childTail;
expect(tooltipScope.isOpen).toBeFalsy();
trigger(elm, 'fakeTrigger');
expect(tooltipScope.isOpen).toBeTruthy();
trigger(elm, 'fakeTrigger');
expect(tooltipScope.isOpen).toBeFalsy();
}));
});
});
describe('placementClassPrefix', function() {
beforeEach(module('ui.bootstrap.tooltip', function($uibTooltipProvider) {
$uibTooltipProvider.options({placementClassPrefix: 'uib-'});
}));
// load the template
beforeEach(module('uib/template/tooltip/tooltip-popup.html'));
it('should add the classes', inject(function($rootScope, $compile, $timeout) {
elmBody = angular.element(
'
'
);
scope = $rootScope;
$compile(elmBody)(scope);
scope.$digest();
elm = elmBody.find('span');
elmScope = elm.scope();
tooltipScope = elmScope.$$childTail;
expect(elmBody.children().length).toBe(1);
trigger(elm, 'mouseenter');
$timeout.flush();
var tooltipElm = elmBody.find('.tooltip');
expect(tooltipElm.hasClass('top')).toBe(true);
expect(tooltipElm.hasClass('uib-top-right')).toBe(true);
}));
});
});
================================================
FILE: src/tooltip/test/tooltip2.spec.js
================================================
describe('tooltip directive', function() {
var $rootScope, $compile, $document, $timeout, body, fragment;
beforeEach(module('ui.bootstrap.tooltip'));
beforeEach(module('uib/template/tooltip/tooltip-popup.html'));
beforeEach(module('uib/template/tooltip/tooltip-template-popup.html'));
beforeEach(module('uib/template/tooltip/tooltip-html-popup.html'));
beforeEach(inject(function(_$rootScope_, _$compile_, _$document_, _$timeout_) {
$rootScope = _$rootScope_;
$compile = _$compile_;
$document = _$document_;
$timeout = _$timeout_;
body = $document.find('body');
}));
beforeEach(function() {
jasmine.addMatchers({
toHaveOpenTooltips: function(util, customEqualityTesters) {
return {
compare: function(actual, noOfOpened) {
var ttipElements = actual.find('div.tooltip');
noOfOpened = noOfOpened || 1;
var result = {
pass: util.equals(ttipElements.length, noOfOpened, customEqualityTesters)
};
if (result.pass) {
result.message = 'Expected "' + angular.mock.dump(ttipElements) + '" not to have "' + ttipElements.length + '" opened tooltips.';
} else {
result.message = 'Expected "' + angular.mock.dump(ttipElements) + '" to have "' + ttipElements.length + '" opened tooltips.';
}
return result;
}
};
}
});
});
afterEach(function() {
$document.off('keypress');
fragment.remove();
});
function compileTooltip(ttipMarkup) {
fragment = $compile('
' + ttipMarkup + '
')($rootScope);
$rootScope.$digest();
body.append(fragment);
}
function closeTooltip(hostEl, triggerEvt, shouldNotFlush) {
trigger(hostEl, triggerEvt || 'mouseleave');
hostEl.scope().$$childTail.$digest();
if (!shouldNotFlush) {
$timeout.flush();
}
}
function trigger(element, evt) {
element.trigger(evt);
element.scope().$$childTail.$digest();
}
describe('basic scenarios with default options', function() {
it('shows default tooltip on mouse enter and closes on mouse leave', function() {
compileTooltip('
Trigger here');
trigger(fragment.find('span'), 'mouseenter');
expect(fragment).toHaveOpenTooltips();
closeTooltip(fragment.find('span'));
expect(fragment).not.toHaveOpenTooltips();
});
it('should not show a tooltip when its content is empty', function() {
compileTooltip('
');
trigger(fragment.find('span'), 'mouseenter');
expect(fragment).not.toHaveOpenTooltips();
});
it('should not show a tooltip when its content becomes empty', function() {
$rootScope.content = 'some text';
compileTooltip('
');
trigger(fragment.find('span'), 'mouseenter');
$timeout.flush(0);
expect(fragment).toHaveOpenTooltips();
$rootScope.content = '';
$rootScope.$digest();
$timeout.flush();
expect(fragment).not.toHaveOpenTooltips();
});
it('should update tooltip when its content becomes empty', function() {
$rootScope.content = 'some text';
compileTooltip('
');
$rootScope.content = '';
$rootScope.$digest();
trigger(fragment.find('span'), 'mouseenter');
expect(fragment).not.toHaveOpenTooltips();
});
});
describe('option by option', function() {
var tooltipTypes = {
'tooltip': 'uib-tooltip="tooltip text"',
'tooltip-html': 'uib-tooltip-html="tooltipSafeHtml"',
'tooltip-template': 'uib-tooltip-template="\'tooltipTextUrl\'"'
};
beforeEach(inject(function($sce, $templateCache) {
$rootScope.tooltipText = 'tooltip text';
$rootScope.tooltipSafeHtml = $sce.trustAsHtml('tooltip text');
$templateCache.put('tooltipTextUrl', [200, '
tooltip text', {}]);
}));
angular.forEach(tooltipTypes, function(html, key) {
describe(key, function() {
describe('placement', function() {
it('can specify an alternative, valid placement', function() {
compileTooltip('
Trigger here');
trigger(fragment.find('span'), 'mouseenter');
var ttipElement = fragment.find('div.tooltip');
expect(fragment).toHaveOpenTooltips();
expect(ttipElement).toHaveClass('left');
closeTooltip(fragment.find('span'));
expect(fragment).not.toHaveOpenTooltips();
});
});
describe('class', function() {
it('can specify a custom class', function() {
compileTooltip('
Trigger here');
trigger(fragment.find('span'), 'mouseenter');
var ttipElement = fragment.find('div.tooltip');
expect(fragment).toHaveOpenTooltips();
expect(ttipElement).toHaveClass('custom');
closeTooltip(fragment.find('span'));
expect(fragment).not.toHaveOpenTooltips();
});
});
});
});
});
it('should show even after close trigger is called multiple times - issue #1847', function() {
compileTooltip('
Trigger here');
trigger(fragment.find('span'), 'mouseenter');
expect(fragment).toHaveOpenTooltips();
closeTooltip(fragment.find('span'), null, true);
// Close trigger is called again before timer completes
// The close trigger can be called any number of times (even after close has already been called)
// since users can trigger the hide triggers manually.
closeTooltip(fragment.find('span'), null, true);
expect(fragment).toHaveOpenTooltips();
trigger(fragment.find('span'), 'mouseenter');
expect(fragment).toHaveOpenTooltips();
$timeout.flush();
expect(fragment).toHaveOpenTooltips();
});
it('should hide even after show trigger is called multiple times', function() {
compileTooltip('
Trigger here');
trigger(fragment.find('span'), 'mouseenter');
trigger(fragment.find('span'), 'mouseenter');
closeTooltip(fragment.find('span'));
expect(fragment).not.toHaveOpenTooltips();
});
it('should not show tooltips element is disabled (button) - issue #3167', function() {
compileTooltip('
');
trigger(fragment.find('button'), 'mouseenter');
expect(fragment).toHaveOpenTooltips();
trigger(fragment.find('button'), 'click');
$timeout.flush();
// One needs to flush deferred functions before checking there is no tooltip.
expect(fragment).not.toHaveOpenTooltips();
});
});
================================================
FILE: src/tooltip/tooltip.css
================================================
[uib-tooltip-popup].tooltip.top-left > .tooltip-arrow,
[uib-tooltip-popup].tooltip.top-right > .tooltip-arrow,
[uib-tooltip-popup].tooltip.bottom-left > .tooltip-arrow,
[uib-tooltip-popup].tooltip.bottom-right > .tooltip-arrow,
[uib-tooltip-popup].tooltip.left-top > .tooltip-arrow,
[uib-tooltip-popup].tooltip.left-bottom > .tooltip-arrow,
[uib-tooltip-popup].tooltip.right-top > .tooltip-arrow,
[uib-tooltip-popup].tooltip.right-bottom > .tooltip-arrow,
[uib-tooltip-html-popup].tooltip.top-left > .tooltip-arrow,
[uib-tooltip-html-popup].tooltip.top-right > .tooltip-arrow,
[uib-tooltip-html-popup].tooltip.bottom-left > .tooltip-arrow,
[uib-tooltip-html-popup].tooltip.bottom-right > .tooltip-arrow,
[uib-tooltip-html-popup].tooltip.left-top > .tooltip-arrow,
[uib-tooltip-html-popup].tooltip.left-bottom > .tooltip-arrow,
[uib-tooltip-html-popup].tooltip.right-top > .tooltip-arrow,
[uib-tooltip-html-popup].tooltip.right-bottom > .tooltip-arrow,
[uib-tooltip-template-popup].tooltip.top-left > .tooltip-arrow,
[uib-tooltip-template-popup].tooltip.top-right > .tooltip-arrow,
[uib-tooltip-template-popup].tooltip.bottom-left > .tooltip-arrow,
[uib-tooltip-template-popup].tooltip.bottom-right > .tooltip-arrow,
[uib-tooltip-template-popup].tooltip.left-top > .tooltip-arrow,
[uib-tooltip-template-popup].tooltip.left-bottom > .tooltip-arrow,
[uib-tooltip-template-popup].tooltip.right-top > .tooltip-arrow,
[uib-tooltip-template-popup].tooltip.right-bottom > .tooltip-arrow,
[uib-popover-popup].popover.top-left > .arrow,
[uib-popover-popup].popover.top-right > .arrow,
[uib-popover-popup].popover.bottom-left > .arrow,
[uib-popover-popup].popover.bottom-right > .arrow,
[uib-popover-popup].popover.left-top > .arrow,
[uib-popover-popup].popover.left-bottom > .arrow,
[uib-popover-popup].popover.right-top > .arrow,
[uib-popover-popup].popover.right-bottom > .arrow,
[uib-popover-html-popup].popover.top-left > .arrow,
[uib-popover-html-popup].popover.top-right > .arrow,
[uib-popover-html-popup].popover.bottom-left > .arrow,
[uib-popover-html-popup].popover.bottom-right > .arrow,
[uib-popover-html-popup].popover.left-top > .arrow,
[uib-popover-html-popup].popover.left-bottom > .arrow,
[uib-popover-html-popup].popover.right-top > .arrow,
[uib-popover-html-popup].popover.right-bottom > .arrow,
[uib-popover-template-popup].popover.top-left > .arrow,
[uib-popover-template-popup].popover.top-right > .arrow,
[uib-popover-template-popup].popover.bottom-left > .arrow,
[uib-popover-template-popup].popover.bottom-right > .arrow,
[uib-popover-template-popup].popover.left-top > .arrow,
[uib-popover-template-popup].popover.left-bottom > .arrow,
[uib-popover-template-popup].popover.right-top > .arrow,
[uib-popover-template-popup].popover.right-bottom > .arrow {
top: auto;
bottom: auto;
left: auto;
right: auto;
margin: 0;
}
[uib-popover-popup].popover,
[uib-popover-html-popup].popover,
[uib-popover-template-popup].popover {
display: block !important;
}
================================================
FILE: src/tooltip/tooltip.js
================================================
/**
* The following features are still outstanding: animation as a
* function, placement as a function, inside, support for more triggers than
* just mouse enter/leave, html tooltips, and selector delegation.
*/
angular.module('ui.bootstrap.tooltip', ['ui.bootstrap.position', 'ui.bootstrap.stackedMap'])
/**
* The $tooltip service creates tooltip- and popover-like directives as well as
* houses global options for them.
*/
.provider('$uibTooltip', function() {
// The default options tooltip and popover.
var defaultOptions = {
placement: 'top',
placementClassPrefix: '',
animation: true,
popupDelay: 0,
popupCloseDelay: 0,
useContentExp: false
};
// Default hide triggers for each show trigger
var triggerMap = {
'mouseenter': 'mouseleave',
'click': 'click',
'outsideClick': 'outsideClick',
'focus': 'blur',
'none': ''
};
// The options specified to the provider globally.
var globalOptions = {};
/**
* `options({})` allows global configuration of all tooltips in the
* application.
*
* var app = angular.module( 'App', ['ui.bootstrap.tooltip'], function( $tooltipProvider ) {
* // place tooltips left instead of top by default
* $tooltipProvider.options( { placement: 'left' } );
* });
*/
this.options = function(value) {
angular.extend(globalOptions, value);
};
/**
* This allows you to extend the set of trigger mappings available. E.g.:
*
* $tooltipProvider.setTriggers( { 'openTrigger': 'closeTrigger' } );
*/
this.setTriggers = function setTriggers(triggers) {
angular.extend(triggerMap, triggers);
};
/**
* This is a helper function for translating camel-case to snake_case.
*/
function snake_case(name) {
var regexp = /[A-Z]/g;
var separator = '-';
return name.replace(regexp, function(letter, pos) {
return (pos ? separator : '') + letter.toLowerCase();
});
}
/**
* Returns the actual instance of the $tooltip service.
* TODO support multiple triggers
*/
this.$get = ['$window', '$compile', '$timeout', '$document', '$uibPosition', '$interpolate', '$rootScope', '$parse', '$$stackedMap', function($window, $compile, $timeout, $document, $position, $interpolate, $rootScope, $parse, $$stackedMap) {
var openedTooltips = $$stackedMap.createNew();
$document.on('keyup', keypressListener);
$rootScope.$on('$destroy', function() {
$document.off('keyup', keypressListener);
});
function keypressListener(e) {
if (e.which === 27) {
var last = openedTooltips.top();
if (last) {
last.value.close();
last = null;
}
}
}
return function $tooltip(ttType, prefix, defaultTriggerShow, options) {
options = angular.extend({}, defaultOptions, globalOptions, options);
/**
* Returns an object of show and hide triggers.
*
* If a trigger is supplied,
* it is used to show the tooltip; otherwise, it will use the `trigger`
* option passed to the `$tooltipProvider.options` method; else it will
* default to the trigger supplied to this directive factory.
*
* The hide trigger is based on the show trigger. If the `trigger` option
* was passed to the `$tooltipProvider.options` method, it will use the
* mapped trigger from `triggerMap` or the passed trigger if the map is
* undefined; otherwise, it uses the `triggerMap` value of the show
* trigger; else it will just use the show trigger.
*/
function getTriggers(trigger) {
var show = (trigger || options.trigger || defaultTriggerShow).split(' ');
var hide = show.map(function(trigger) {
return triggerMap[trigger] || trigger;
});
return {
show: show,
hide: hide
};
}
var directiveName = snake_case(ttType);
var startSym = $interpolate.startSymbol();
var endSym = $interpolate.endSymbol();
var template =
'
' +
'
';
return {
compile: function(tElem, tAttrs) {
var tooltipLinker = $compile(template);
return function link(scope, element, attrs, tooltipCtrl) {
var tooltip;
var tooltipLinkedScope;
var transitionTimeout;
var showTimeout;
var hideTimeout;
var positionTimeout;
var adjustmentTimeout;
var appendToBody = angular.isDefined(options.appendToBody) ? options.appendToBody : false;
var triggers = getTriggers(undefined);
var hasEnableExp = angular.isDefined(attrs[prefix + 'Enable']);
var ttScope = scope.$new(true);
var repositionScheduled = false;
var isOpenParse = angular.isDefined(attrs[prefix + 'IsOpen']) ? $parse(attrs[prefix + 'IsOpen']) : false;
var contentParse = options.useContentExp ? $parse(attrs[ttType]) : false;
var observers = [];
var lastPlacement;
var positionTooltip = function() {
// check if tooltip exists and is not empty
if (!tooltip || !tooltip.html()) { return; }
if (!positionTimeout) {
positionTimeout = $timeout(function() {
var ttPosition = $position.positionElements(element, tooltip, ttScope.placement, appendToBody);
var initialHeight = angular.isDefined(tooltip.offsetHeight) ? tooltip.offsetHeight : tooltip.prop('offsetHeight');
var elementPos = appendToBody ? $position.offset(element) : $position.position(element);
tooltip.css({ top: ttPosition.top + 'px', left: ttPosition.left + 'px' });
var placementClasses = ttPosition.placement.split('-');
if (!tooltip.hasClass(placementClasses[0])) {
tooltip.removeClass(lastPlacement.split('-')[0]);
tooltip.addClass(placementClasses[0]);
}
if (!tooltip.hasClass(options.placementClassPrefix + ttPosition.placement)) {
tooltip.removeClass(options.placementClassPrefix + lastPlacement);
tooltip.addClass(options.placementClassPrefix + ttPosition.placement);
}
adjustmentTimeout = $timeout(function() {
var currentHeight = angular.isDefined(tooltip.offsetHeight) ? tooltip.offsetHeight : tooltip.prop('offsetHeight');
var adjustment = $position.adjustTop(placementClasses, elementPos, initialHeight, currentHeight);
if (adjustment) {
tooltip.css(adjustment);
}
adjustmentTimeout = null;
}, 0, false);
// first time through tt element will have the
// uib-position-measure class or if the placement
// has changed we need to position the arrow.
if (tooltip.hasClass('uib-position-measure')) {
$position.positionArrow(tooltip, ttPosition.placement);
tooltip.removeClass('uib-position-measure');
} else if (lastPlacement !== ttPosition.placement) {
$position.positionArrow(tooltip, ttPosition.placement);
}
lastPlacement = ttPosition.placement;
positionTimeout = null;
}, 0, false);
}
};
// Set up the correct scope to allow transclusion later
ttScope.origScope = scope;
// By default, the tooltip is not open.
// TODO add ability to start tooltip opened
ttScope.isOpen = false;
function toggleTooltipBind() {
if (!ttScope.isOpen) {
showTooltipBind();
} else {
hideTooltipBind();
}
}
// Show the tooltip with delay if specified, otherwise show it immediately
function showTooltipBind() {
if (hasEnableExp && !scope.$eval(attrs[prefix + 'Enable'])) {
return;
}
cancelHide();
prepareTooltip();
if (ttScope.popupDelay) {
// Do nothing if the tooltip was already scheduled to pop-up.
// This happens if show is triggered multiple times before any hide is triggered.
if (!showTimeout) {
showTimeout = $timeout(show, ttScope.popupDelay, false);
}
} else {
show();
}
}
function hideTooltipBind() {
cancelShow();
if (ttScope.popupCloseDelay) {
if (!hideTimeout) {
hideTimeout = $timeout(hide, ttScope.popupCloseDelay, false);
}
} else {
hide();
}
}
// Show the tooltip popup element.
function show() {
cancelShow();
cancelHide();
// Don't show empty tooltips.
if (!ttScope.content) {
return angular.noop;
}
createTooltip();
// And show the tooltip.
ttScope.$evalAsync(function() {
ttScope.isOpen = true;
assignIsOpen(true);
positionTooltip();
});
}
function cancelShow() {
if (showTimeout) {
$timeout.cancel(showTimeout);
showTimeout = null;
}
if (positionTimeout) {
$timeout.cancel(positionTimeout);
positionTimeout = null;
}
}
// Hide the tooltip popup element.
function hide() {
if (!ttScope) {
return;
}
// First things first: we don't show it anymore.
ttScope.$evalAsync(function() {
if (ttScope) {
ttScope.isOpen = false;
assignIsOpen(false);
// And now we remove it from the DOM. However, if we have animation, we
// need to wait for it to expire beforehand.
// FIXME: this is a placeholder for a port of the transitions library.
// The fade transition in TWBS is 150ms.
if (ttScope.animation) {
if (!transitionTimeout) {
transitionTimeout = $timeout(removeTooltip, 150, false);
}
} else {
removeTooltip();
}
}
});
}
function cancelHide() {
if (hideTimeout) {
$timeout.cancel(hideTimeout);
hideTimeout = null;
}
if (transitionTimeout) {
$timeout.cancel(transitionTimeout);
transitionTimeout = null;
}
}
function createTooltip() {
// There can only be one tooltip element per directive shown at once.
if (tooltip) {
return;
}
tooltipLinkedScope = ttScope.$new();
tooltip = tooltipLinker(tooltipLinkedScope, function(tooltip) {
if (appendToBody) {
$document.find('body').append(tooltip);
} else {
element.after(tooltip);
}
});
openedTooltips.add(ttScope, {
close: hide
});
prepObservers();
}
function removeTooltip() {
cancelShow();
cancelHide();
unregisterObservers();
if (tooltip) {
tooltip.remove();
tooltip = null;
if (adjustmentTimeout) {
$timeout.cancel(adjustmentTimeout);
}
}
openedTooltips.remove(ttScope);
if (tooltipLinkedScope) {
tooltipLinkedScope.$destroy();
tooltipLinkedScope = null;
}
}
/**
* Set the initial scope values. Once
* the tooltip is created, the observers
* will be added to keep things in sync.
*/
function prepareTooltip() {
ttScope.title = attrs[prefix + 'Title'];
if (contentParse) {
ttScope.content = contentParse(scope);
} else {
ttScope.content = attrs[ttType];
}
ttScope.popupClass = attrs[prefix + 'Class'];
ttScope.placement = angular.isDefined(attrs[prefix + 'Placement']) ? attrs[prefix + 'Placement'] : options.placement;
var placement = $position.parsePlacement(ttScope.placement);
lastPlacement = placement[1] ? placement[0] + '-' + placement[1] : placement[0];
var delay = parseInt(attrs[prefix + 'PopupDelay'], 10);
var closeDelay = parseInt(attrs[prefix + 'PopupCloseDelay'], 10);
ttScope.popupDelay = !isNaN(delay) ? delay : options.popupDelay;
ttScope.popupCloseDelay = !isNaN(closeDelay) ? closeDelay : options.popupCloseDelay;
}
function assignIsOpen(isOpen) {
if (isOpenParse && angular.isFunction(isOpenParse.assign)) {
isOpenParse.assign(scope, isOpen);
}
}
ttScope.contentExp = function() {
return ttScope.content;
};
/**
* Observe the relevant attributes.
*/
attrs.$observe('disabled', function(val) {
if (val) {
cancelShow();
}
if (val && ttScope.isOpen) {
hide();
}
});
if (isOpenParse) {
scope.$watch(isOpenParse, function(val) {
if (ttScope && !val === ttScope.isOpen) {
toggleTooltipBind();
}
});
}
function prepObservers() {
observers.length = 0;
if (contentParse) {
observers.push(
scope.$watch(contentParse, function(val) {
ttScope.content = val;
if (!val && ttScope.isOpen) {
hide();
}
})
);
observers.push(
tooltipLinkedScope.$watch(function() {
if (!repositionScheduled) {
repositionScheduled = true;
tooltipLinkedScope.$$postDigest(function() {
repositionScheduled = false;
if (ttScope && ttScope.isOpen) {
positionTooltip();
}
});
}
})
);
} else {
observers.push(
attrs.$observe(ttType, function(val) {
ttScope.content = val;
if (!val && ttScope.isOpen) {
hide();
} else {
positionTooltip();
}
})
);
}
observers.push(
attrs.$observe(prefix + 'Title', function(val) {
ttScope.title = val;
if (ttScope.isOpen) {
positionTooltip();
}
})
);
observers.push(
attrs.$observe(prefix + 'Placement', function(val) {
ttScope.placement = val ? val : options.placement;
if (ttScope.isOpen) {
positionTooltip();
}
})
);
}
function unregisterObservers() {
if (observers.length) {
angular.forEach(observers, function(observer) {
observer();
});
observers.length = 0;
}
}
// hide tooltips/popovers for outsideClick trigger
function bodyHideTooltipBind(e) {
if (!ttScope || !ttScope.isOpen || !tooltip) {
return;
}
// make sure the tooltip/popover link or tool tooltip/popover itself were not clicked
if (!element[0].contains(e.target) && !tooltip[0].contains(e.target)) {
hideTooltipBind();
}
}
// KeyboardEvent handler to hide the tooltip on Escape key press
function hideOnEscapeKey(e) {
if (e.which === 27) {
hideTooltipBind();
}
}
var unregisterTriggers = function() {
triggers.show.forEach(function(trigger) {
if (trigger === 'outsideClick') {
element.off('click', toggleTooltipBind);
} else {
element.off(trigger, showTooltipBind);
element.off(trigger, toggleTooltipBind);
}
element.off('keypress', hideOnEscapeKey);
});
triggers.hide.forEach(function(trigger) {
if (trigger === 'outsideClick') {
$document.off('click', bodyHideTooltipBind);
} else {
element.off(trigger, hideTooltipBind);
}
});
};
function prepTriggers() {
var showTriggers = [], hideTriggers = [];
var val = scope.$eval(attrs[prefix + 'Trigger']);
unregisterTriggers();
if (angular.isObject(val)) {
Object.keys(val).forEach(function(key) {
showTriggers.push(key);
hideTriggers.push(val[key]);
});
triggers = {
show: showTriggers,
hide: hideTriggers
};
} else {
triggers = getTriggers(val);
}
if (triggers.show !== 'none') {
triggers.show.forEach(function(trigger, idx) {
if (trigger === 'outsideClick') {
element.on('click', toggleTooltipBind);
$document.on('click', bodyHideTooltipBind);
} else if (trigger === triggers.hide[idx]) {
element.on(trigger, toggleTooltipBind);
} else if (trigger) {
element.on(trigger, showTooltipBind);
element.on(triggers.hide[idx], hideTooltipBind);
}
element.on('keypress', hideOnEscapeKey);
});
}
}
prepTriggers();
var animation = scope.$eval(attrs[prefix + 'Animation']);
ttScope.animation = angular.isDefined(animation) ? !!animation : options.animation;
var appendToBodyVal;
var appendKey = prefix + 'AppendToBody';
if (appendKey in attrs && attrs[appendKey] === undefined) {
appendToBodyVal = true;
} else {
appendToBodyVal = scope.$eval(attrs[appendKey]);
}
appendToBody = angular.isDefined(appendToBodyVal) ? appendToBodyVal : appendToBody;
// Make sure tooltip is destroyed and removed.
scope.$on('$destroy', function onDestroyTooltip() {
unregisterTriggers();
removeTooltip();
ttScope = null;
});
};
}
};
};
}];
})
// This is mostly ngInclude code but with a custom scope
.directive('uibTooltipTemplateTransclude', [
'$animate', '$sce', '$compile', '$templateRequest',
function ($animate, $sce, $compile, $templateRequest) {
return {
link: function(scope, elem, attrs) {
var origScope = scope.$eval(attrs.tooltipTemplateTranscludeScope);
var changeCounter = 0,
currentScope,
previousElement,
currentElement;
var cleanupLastIncludeContent = function() {
if (previousElement) {
previousElement.remove();
previousElement = null;
}
if (currentScope) {
currentScope.$destroy();
currentScope = null;
}
if (currentElement) {
$animate.leave(currentElement).then(function() {
previousElement = null;
});
previousElement = currentElement;
currentElement = null;
}
};
scope.$watch($sce.parseAsResourceUrl(attrs.uibTooltipTemplateTransclude), function(src) {
var thisChangeId = ++changeCounter;
if (src) {
//set the 2nd param to true to ignore the template request error so that the inner
//contents and scope can be cleaned up.
$templateRequest(src, true).then(function(response) {
if (thisChangeId !== changeCounter) { return; }
var newScope = origScope.$new();
var template = response;
var clone = $compile(template)(newScope, function(clone) {
cleanupLastIncludeContent();
$animate.enter(clone, elem);
});
currentScope = newScope;
currentElement = clone;
currentScope.$emit('$includeContentLoaded', src);
}, function() {
if (thisChangeId === changeCounter) {
cleanupLastIncludeContent();
scope.$emit('$includeContentError', src);
}
});
scope.$emit('$includeContentRequested', src);
} else {
cleanupLastIncludeContent();
}
});
scope.$on('$destroy', cleanupLastIncludeContent);
}
};
}])
/**
* Note that it's intentional that these classes are *not* applied through $animate.
* They must not be animated as they're expected to be present on the tooltip on
* initialization.
*/
.directive('uibTooltipClasses', ['$uibPosition', function($uibPosition) {
return {
restrict: 'A',
link: function(scope, element, attrs) {
// need to set the primary position so the
// arrow has space during position measure.
// tooltip.positionTooltip()
if (scope.placement) {
// // There are no top-left etc... classes
// // in TWBS, so we need the primary position.
var position = $uibPosition.parsePlacement(scope.placement);
element.addClass(position[0]);
}
if (scope.popupClass) {
element.addClass(scope.popupClass);
}
if (scope.animation) {
element.addClass(attrs.tooltipAnimationClass);
}
}
};
}])
.directive('uibTooltipPopup', function() {
return {
restrict: 'A',
scope: { content: '@' },
templateUrl: 'uib/template/tooltip/tooltip-popup.html'
};
})
.directive('uibTooltip', [ '$uibTooltip', function($uibTooltip) {
return $uibTooltip('uibTooltip', 'tooltip', 'mouseenter');
}])
.directive('uibTooltipTemplatePopup', function() {
return {
restrict: 'A',
scope: { contentExp: '&', originScope: '&' },
templateUrl: 'uib/template/tooltip/tooltip-template-popup.html'
};
})
.directive('uibTooltipTemplate', ['$uibTooltip', function($uibTooltip) {
return $uibTooltip('uibTooltipTemplate', 'tooltip', 'mouseenter', {
useContentExp: true
});
}])
.directive('uibTooltipHtmlPopup', function() {
return {
restrict: 'A',
scope: { contentExp: '&' },
templateUrl: 'uib/template/tooltip/tooltip-html-popup.html'
};
})
.directive('uibTooltipHtml', ['$uibTooltip', function($uibTooltip) {
return $uibTooltip('uibTooltipHtml', 'tooltip', 'mouseenter', {
useContentExp: true
});
}]);
================================================
FILE: src/typeahead/docs/demo.html
================================================
================================================
FILE: src/typeahead/docs/demo.js
================================================
angular.module('ui.bootstrap.demo').controller('TypeaheadCtrl', function($scope, $http) {
var _selected;
$scope.selected = undefined;
$scope.states = ['Alabama', 'Alaska', 'Arizona', 'Arkansas', 'California', 'Colorado', 'Connecticut', 'Delaware', 'Florida', 'Georgia', 'Hawaii', 'Idaho', 'Illinois', 'Indiana', 'Iowa', 'Kansas', 'Kentucky', 'Louisiana', 'Maine', 'Maryland', 'Massachusetts', 'Michigan', 'Minnesota', 'Mississippi', 'Missouri', 'Montana', 'Nebraska', 'Nevada', 'New Hampshire', 'New Jersey', 'New Mexico', 'New York', 'North Dakota', 'North Carolina', 'Ohio', 'Oklahoma', 'Oregon', 'Pennsylvania', 'Rhode Island', 'South Carolina', 'South Dakota', 'Tennessee', 'Texas', 'Utah', 'Vermont', 'Virginia', 'Washington', 'West Virginia', 'Wisconsin', 'Wyoming'];
// Any function returning a promise object can be used to load values asynchronously
$scope.getLocation = function(val) {
return $http.get('//maps.googleapis.com/maps/api/geocode/json', {
params: {
address: val,
sensor: false
}
}).then(function(response){
return response.data.results.map(function(item){
return item.formatted_address;
});
});
};
$scope.ngModelOptionsSelected = function(value) {
if (arguments.length) {
_selected = value;
} else {
return _selected;
}
};
$scope.modelOptions = {
debounce: {
default: 500,
blur: 250
},
getterSetter: true
};
$scope.statesWithFlags = [{'name':'Alabama','flag':'5/5c/Flag_of_Alabama.svg/45px-Flag_of_Alabama.svg.png'},{'name':'Alaska','flag':'e/e6/Flag_of_Alaska.svg/43px-Flag_of_Alaska.svg.png'},{'name':'Arizona','flag':'9/9d/Flag_of_Arizona.svg/45px-Flag_of_Arizona.svg.png'},{'name':'Arkansas','flag':'9/9d/Flag_of_Arkansas.svg/45px-Flag_of_Arkansas.svg.png'},{'name':'California','flag':'0/01/Flag_of_California.svg/45px-Flag_of_California.svg.png'},{'name':'Colorado','flag':'4/46/Flag_of_Colorado.svg/45px-Flag_of_Colorado.svg.png'},{'name':'Connecticut','flag':'9/96/Flag_of_Connecticut.svg/39px-Flag_of_Connecticut.svg.png'},{'name':'Delaware','flag':'c/c6/Flag_of_Delaware.svg/45px-Flag_of_Delaware.svg.png'},{'name':'Florida','flag':'f/f7/Flag_of_Florida.svg/45px-Flag_of_Florida.svg.png'},{'name':'Georgia','flag':'5/54/Flag_of_Georgia_%28U.S._state%29.svg/46px-Flag_of_Georgia_%28U.S._state%29.svg.png'},{'name':'Hawaii','flag':'e/ef/Flag_of_Hawaii.svg/46px-Flag_of_Hawaii.svg.png'},{'name':'Idaho','flag':'a/a4/Flag_of_Idaho.svg/38px-Flag_of_Idaho.svg.png'},{'name':'Illinois','flag':'0/01/Flag_of_Illinois.svg/46px-Flag_of_Illinois.svg.png'},{'name':'Indiana','flag':'a/ac/Flag_of_Indiana.svg/45px-Flag_of_Indiana.svg.png'},{'name':'Iowa','flag':'a/aa/Flag_of_Iowa.svg/44px-Flag_of_Iowa.svg.png'},{'name':'Kansas','flag':'d/da/Flag_of_Kansas.svg/46px-Flag_of_Kansas.svg.png'},{'name':'Kentucky','flag':'8/8d/Flag_of_Kentucky.svg/46px-Flag_of_Kentucky.svg.png'},{'name':'Louisiana','flag':'e/e0/Flag_of_Louisiana.svg/46px-Flag_of_Louisiana.svg.png'},{'name':'Maine','flag':'3/35/Flag_of_Maine.svg/45px-Flag_of_Maine.svg.png'},{'name':'Maryland','flag':'a/a0/Flag_of_Maryland.svg/45px-Flag_of_Maryland.svg.png'},{'name':'Massachusetts','flag':'f/f2/Flag_of_Massachusetts.svg/46px-Flag_of_Massachusetts.svg.png'},{'name':'Michigan','flag':'b/b5/Flag_of_Michigan.svg/45px-Flag_of_Michigan.svg.png'},{'name':'Minnesota','flag':'b/b9/Flag_of_Minnesota.svg/46px-Flag_of_Minnesota.svg.png'},{'name':'Mississippi','flag':'4/42/Flag_of_Mississippi.svg/45px-Flag_of_Mississippi.svg.png'},{'name':'Missouri','flag':'5/5a/Flag_of_Missouri.svg/46px-Flag_of_Missouri.svg.png'},{'name':'Montana','flag':'c/cb/Flag_of_Montana.svg/45px-Flag_of_Montana.svg.png'},{'name':'Nebraska','flag':'4/4d/Flag_of_Nebraska.svg/46px-Flag_of_Nebraska.svg.png'},{'name':'Nevada','flag':'f/f1/Flag_of_Nevada.svg/45px-Flag_of_Nevada.svg.png'},{'name':'New Hampshire','flag':'2/28/Flag_of_New_Hampshire.svg/45px-Flag_of_New_Hampshire.svg.png'},{'name':'New Jersey','flag':'9/92/Flag_of_New_Jersey.svg/45px-Flag_of_New_Jersey.svg.png'},{'name':'New Mexico','flag':'c/c3/Flag_of_New_Mexico.svg/45px-Flag_of_New_Mexico.svg.png'},{'name':'New York','flag':'1/1a/Flag_of_New_York.svg/46px-Flag_of_New_York.svg.png'},{'name':'North Carolina','flag':'b/bb/Flag_of_North_Carolina.svg/45px-Flag_of_North_Carolina.svg.png'},{'name':'North Dakota','flag':'e/ee/Flag_of_North_Dakota.svg/38px-Flag_of_North_Dakota.svg.png'},{'name':'Ohio','flag':'4/4c/Flag_of_Ohio.svg/46px-Flag_of_Ohio.svg.png'},{'name':'Oklahoma','flag':'6/6e/Flag_of_Oklahoma.svg/45px-Flag_of_Oklahoma.svg.png'},{'name':'Oregon','flag':'b/b9/Flag_of_Oregon.svg/46px-Flag_of_Oregon.svg.png'},{'name':'Pennsylvania','flag':'f/f7/Flag_of_Pennsylvania.svg/45px-Flag_of_Pennsylvania.svg.png'},{'name':'Rhode Island','flag':'f/f3/Flag_of_Rhode_Island.svg/32px-Flag_of_Rhode_Island.svg.png'},{'name':'South Carolina','flag':'6/69/Flag_of_South_Carolina.svg/45px-Flag_of_South_Carolina.svg.png'},{'name':'South Dakota','flag':'1/1a/Flag_of_South_Dakota.svg/46px-Flag_of_South_Dakota.svg.png'},{'name':'Tennessee','flag':'9/9e/Flag_of_Tennessee.svg/46px-Flag_of_Tennessee.svg.png'},{'name':'Texas','flag':'f/f7/Flag_of_Texas.svg/45px-Flag_of_Texas.svg.png'},{'name':'Utah','flag':'f/f6/Flag_of_Utah.svg/45px-Flag_of_Utah.svg.png'},{'name':'Vermont','flag':'4/49/Flag_of_Vermont.svg/46px-Flag_of_Vermont.svg.png'},{'name':'Virginia','flag':'4/47/Flag_of_Virginia.svg/44px-Flag_of_Virginia.svg.png'},{'name':'Washington','flag':'5/54/Flag_of_Washington.svg/46px-Flag_of_Washington.svg.png'},{'name':'West Virginia','flag':'2/22/Flag_of_West_Virginia.svg/46px-Flag_of_West_Virginia.svg.png'},{'name':'Wisconsin','flag':'2/22/Flag_of_Wisconsin.svg/45px-Flag_of_Wisconsin.svg.png'},{'name':'Wyoming','flag':'b/bc/Flag_of_Wyoming.svg/43px-Flag_of_Wyoming.svg.png'}];
});
================================================
FILE: src/typeahead/docs/readme.md
================================================
Typeahead is a AngularJS version of [Bootstrap v2's typeahead plugin](http://getbootstrap.com/2.3.2/javascript.html#typeahead).
This directive can be used to quickly create elegant typeaheads with any form text input.
It is very well integrated into AngularJS as it uses a subset of the
[select directive](http://docs.angularjs.org/api/ng.directive:select) syntax, which is very flexible. Supported expressions are:
* _label_ for _value_ in _sourceArray_
* _select_ as _label_ for _value_ in _sourceArray_
The `sourceArray` expression can use a special `$viewValue` variable that corresponds to the value entered inside the input.
This directive works with promises, meaning you can retrieve matches using the `$http` service with minimal effort.
### uib-typeahead settings
* `ng-model`
$
-
Assignable angular expression to data-bind to.
* `ng-model-options`
$ -
Options for ng-model (see [ng-model-options directive](https://docs.angularjs.org/api/ng/directive/ngModelOptions)). Currently supports the `debounce` and `getterSetter` options.
* `typeahead-append-to`
$
_(Default: `null`)_ -
Should the typeahead popup be appended to an element instead of the parent element?
* `typeahead-append-to-body`
$
_(Default: `false`)_ -
Should the typeahead popup be appended to $body instead of the parent element?
* `typeahead-editable`
$
_(Default: `true`)_ -
Should it restrict model values to the ones selected from the popup only?
* `typeahead-focus-first`
$
_(Default: `true`)_ -
Should the first match automatically be focused as you type?
* `typeahead-focus-on-select`
_(Default: `true`)_ -
On selection, focus the input element the typeahead directive is associated with.
* `typeahead-input-formatter`
_(Default: `undefined`)_ -
Format the ng-model result after selection.
* `typeahead-is-open`
$
_(Default: `angular.noop`)_ -
Binding to a variable that indicates if the dropdown is open.
* `typeahead-loading`
$
_(Default: `angular.noop`)_ -
Binding to a variable that indicates if matches are being retrieved asynchronously.
* `typeahead-min-length`
$
_(Default: `1`)_ -
Minimal no of characters that needs to be entered before typeahead kicks-in. Must be greater than or equal to 0.
* `typeahead-no-results`
$
_(Default: `angular.noop`)_ -
Binding to a variable that indicates if no matching results were found.
* `typeahead-should-select($event)`
$
_(Default: `null`)_ -
A callback executed when a `keyup` event that might trigger a selection occurs. Selection will only occur if this function returns true.
* `typeahead-on-select($item, $model, $label, $event)`
$
_(Default: `null`)_ -
A callback executed when a match is selected. $event can be undefined if selection not triggered from a user event.
* `typeahead-popup-template-url`
_(Default: `uib/template/typeahead/typeahead-popup.html`)_ -
Set custom popup template.
* `typeahead-select-on-blur`
$
_(Default: `false`)_ -
On blur, select the currently highlighted match.
* `typeahead-select-on-exact`
$
_(Default: `false`)_ -
Automatically select the item when it is the only one that exactly matches the user input.
* `typeahead-show-hint`
$
_(Default: `false`)_ -
Show hint when the first option matches.
* `typeahead-template-url`
_(Default: `uib/template/typeahead/typeahead-match.html`)_ -
Set custom item template.
* `typeahead-wait-ms`
$
_(Default: `0`)_ -
Minimal wait time after last character typed before typeahead kicks-in.
* `uib-typeahead`
$
-
Comprehension Angular expression (see [select directive](http://docs.angularjs.org/api/ng.directive:select)).
**Notes**
If a custom template for the popup is used, the wrapper selector used for the match items is the `uib-typeahead-match` class.
================================================
FILE: src/typeahead/index-nocss.js
================================================
require('../debounce');
require('../position/index-nocss.js');
require('../../template/typeahead/typeahead-match.html.js');
require('../../template/typeahead/typeahead-popup.html.js');
require('./typeahead');
var MODULE_NAME = 'ui.bootstrap.module.typeahead';
angular.module(MODULE_NAME, ['ui.bootstrap.typeahead', 'uib/template/typeahead/typeahead-match.html', 'uib/template/typeahead/typeahead-popup.html']);
module.exports = MODULE_NAME;
================================================
FILE: src/typeahead/index.js
================================================
require('../position/position.css');
require('./typeahead.css');
module.exports = require('./index-nocss.js');
================================================
FILE: src/typeahead/test/typeahead-highlight-ngsanitize.spec.js
================================================
describe('Security concerns', function() {
var highlightFilter, $sanitize, logSpy;
beforeEach(module('ui.bootstrap.typeahead', 'ngSanitize'));
beforeEach(inject(function (uibTypeaheadHighlightFilter, _$sanitize_, $log) {
highlightFilter = uibTypeaheadHighlightFilter;
$sanitize = _$sanitize_;
logSpy = spyOn($log, 'warn');
}));
it('should not call the $log service when ngSanitize is present', function() {
highlightFilter('before after', 'match');
expect(logSpy).not.toHaveBeenCalled();
});
});
================================================
FILE: src/typeahead/test/typeahead-highlight.spec.js
================================================
describe('typeaheadHighlight', function () {
var highlightFilter, $log, $sce, logSpy;
beforeEach(module('ui.bootstrap.typeahead'));
beforeEach(inject(function(_$log_, _$sce_) {
$log = _$log_;
$sce = _$sce_;
logSpy = spyOn($log, 'warn');
}));
beforeEach(inject(function(uibTypeaheadHighlightFilter) {
highlightFilter = uibTypeaheadHighlightFilter;
}));
it('should higlight a match', function() {
expect($sce.getTrustedHtml(highlightFilter('before match after', 'match'))).toEqual('before
match after');
});
it('should higlight a match with mixed case', function() {
expect($sce.getTrustedHtml(highlightFilter('before MaTch after', 'match'))).toEqual('before
MaTch after');
});
it('should higlight all matches', function() {
expect($sce.getTrustedHtml(highlightFilter('before MaTch after match', 'match'))).toEqual('before
MaTch after
match');
});
it('should do nothing if no match', function() {
expect($sce.getTrustedHtml(highlightFilter('before match after', 'nomatch'))).toEqual('before match after');
});
it('should do nothing if no or empty query', function() {
expect($sce.getTrustedHtml(highlightFilter('before match after', ''))).toEqual('before match after');
expect($sce.getTrustedHtml(highlightFilter('before match after', null))).toEqual('before match after');
expect($sce.getTrustedHtml(highlightFilter('before match after', undefined))).toEqual('before match after');
});
it('issue 316 - should work correctly for regexp reserved words', function() {
expect($sce.getTrustedHtml(highlightFilter('before (match after', '(match'))).toEqual('before
(match after');
});
it('issue 1777 - should work correctly with numeric values', function() {
expect($sce.getTrustedHtml(highlightFilter(123, '2'))).toEqual('1
23');
});
it('should show a warning when this component is being used unsafely', function() {
highlightFilter('
before match after', 'match');
expect(logSpy).toHaveBeenCalled();
});
});
================================================
FILE: src/typeahead/test/typeahead-parser.spec.js
================================================
describe('syntax parser', function() {
var typeaheadParser, scope, filterFilter;
beforeEach(module('ui.bootstrap.typeahead'));
beforeEach(inject(function(_$rootScope_, _filterFilter_, uibTypeaheadParser) {
typeaheadParser = uibTypeaheadParser;
scope = _$rootScope_;
filterFilter = _filterFilter_;
}));
it('should parse the simplest array-based syntax', function() {
scope.states = ['Alabama', 'California', 'Delaware'];
var result = typeaheadParser.parse('state for state in states | filter:$viewValue');
var itemName = result.itemName;
var locals = {$viewValue:'al'};
expect(result.source(scope, locals)).toEqual(['Alabama', 'California']);
locals[itemName] = 'Alabama';
expect(result.viewMapper(scope, locals)).toEqual('Alabama');
expect(result.modelMapper(scope, locals)).toEqual('Alabama');
});
it('should parse the simplest function-based syntax', function() {
scope.getStates = function($viewValue) {
return filterFilter(['Alabama', 'California', 'Delaware'], $viewValue);
};
var result = typeaheadParser.parse('state for state in getStates($viewValue)');
var itemName = result.itemName;
var locals = {$viewValue:'al'};
expect(result.source(scope, locals)).toEqual(['Alabama', 'California']);
locals[itemName] = 'Alabama';
expect(result.viewMapper(scope, locals)).toEqual('Alabama');
expect(result.modelMapper(scope, locals)).toEqual('Alabama');
});
it('should allow to specify custom model mapping that is used as a label as well', function () {
scope.states = [
{code:'AL', name:'Alabama'},
{code:'CA', name:'California'},
{code:'DE', name:'Delaware'}
];
var result = typeaheadParser.parse('state.name for state in states | filter:$viewValue | orderBy:"name":true');
var itemName = result.itemName;
expect(itemName).toEqual('state');
expect(result.source(scope, {$viewValue:'al'})).toEqual([
{code:'CA', name:'California'},
{code:'AL', name:'Alabama'}
]);
var locals = {$viewValue:'al'};
locals[itemName] = {code:'AL', name:'Alabama'};
expect(result.viewMapper(scope, locals)).toEqual('Alabama');
expect(result.modelMapper(scope, locals)).toEqual('Alabama');
});
it('should allow to specify custom view and model mappers', function() {
scope.states = [
{code:'AL', name:'Alabama'},
{code:'CA', name:'California'},
{code:'DE', name:'Delaware'}
];
var result = typeaheadParser.parse('state.code as state.name + " ("+state.code+")" for state in states | filter:$viewValue | orderBy:"name":true');
var itemName = result.itemName;
expect(result.source(scope, {$viewValue:'al'})).toEqual([
{code:'CA', name:'California'},
{code:'AL', name:'Alabama'}
]);
var locals = {$viewValue:'al'};
locals[itemName] = {code:'AL', name:'Alabama'};
expect(result.viewMapper(scope, locals)).toEqual('Alabama (AL)');
expect(result.modelMapper(scope, locals)).toEqual('AL');
});
});
================================================
FILE: src/typeahead/test/typeahead-popup.spec.js
================================================
describe('typeaheadPopup - result rendering', function() {
var scope, $rootScope, $compile;
beforeEach(module('ui.bootstrap.typeahead'));
beforeEach(module('uib/template/typeahead/typeahead-popup.html'));
beforeEach(module('uib/template/typeahead/typeahead-match.html'));
beforeEach(inject(function(_$rootScope_, _$compile_) {
$rootScope = _$rootScope_;
scope = $rootScope.$new();
$compile = _$compile_;
}));
it('should render initial results', function() {
scope.matches = ['foo', 'bar', 'baz'];
scope.active = 1;
var el = $compile('
')(scope);
$rootScope.$digest();
var liElems = el.find('li');
expect(liElems.length).toEqual(3);
expect(liElems.eq(0)).not.toHaveClass('active');
expect(liElems.eq(1)).toHaveClass('active');
expect(liElems.eq(2)).not.toHaveClass('active');
});
it('should change active item on mouseenter', function() {
scope.matches = ['foo', 'bar', 'baz'];
scope.active = 1;
var el = $compile('
')(scope);
$rootScope.$digest();
var liElems = el.find('li');
expect(liElems.eq(1)).toHaveClass('active');
expect(liElems.eq(2)).not.toHaveClass('active');
liElems.eq(2).trigger('mouseenter');
expect(liElems.eq(1)).not.toHaveClass('active');
expect(liElems.eq(2)).toHaveClass('active');
});
it('should select an item on mouse click', function() {
scope.matches = ['foo', 'bar', 'baz'];
scope.active = 1;
$rootScope.select = angular.noop;
spyOn($rootScope, 'select');
var el = $compile('
')(scope);
$rootScope.$digest();
var liElems = el.find('li');
liElems.eq(2).find('a').trigger('click');
expect($rootScope.select).toHaveBeenCalledWith(2);
});
});
================================================
FILE: src/typeahead/test/typeahead.spec.js
================================================
describe('typeahead tests', function() {
var $scope, $compile, $document, $templateCache, $timeout, $window;
var changeInputValueTo;
beforeEach(module('ui.bootstrap.typeahead'));
beforeEach(module('ngSanitize'));
beforeEach(module('uib/template/typeahead/typeahead-popup.html'));
beforeEach(module('uib/template/typeahead/typeahead-match.html'));
beforeEach(module(function($compileProvider) {
$compileProvider.directive('formatter', function() {
return {
require: 'ngModel',
link: function (scope, elm, attrs, ngModelCtrl) {
ngModelCtrl.$formatters.unshift(function(viewVal) {
return 'formatted' + viewVal;
});
}
};
});
$compileProvider.directive('childDirective', function() {
return {
restrict: 'A',
require: '^parentDirective',
link: function(scope, element, attrs, ctrl) {}
};
});
}));
beforeEach(inject(function(_$rootScope_, _$compile_, _$document_, _$templateCache_, _$timeout_, _$window_, $sniffer) {
$scope = _$rootScope_;
$scope.source = ['foo', 'bar', 'baz'];
$scope.states = [
{code: 'AL', name: 'Alaska'},
{code: 'CL', name: 'California'}
];
$compile = _$compile_;
$document = _$document_;
$templateCache = _$templateCache_;
$timeout = _$timeout_;
$window = _$window_;
changeInputValueTo = function(element, value) {
var inputEl = findInput(element);
inputEl.val(value);
inputEl.trigger($sniffer.hasEvent('input') ? 'input' : 'change');
$scope.$digest();
};
}));
//utility functions
var prepareInputEl = function(inputTpl) {
var el = $compile(angular.element(inputTpl))($scope);
$scope.$digest();
return el;
};
var findInput = function(element) {
return element.find('input');
};
var findDropDown = function(element) {
return element.find('ul.dropdown-menu');
};
var findMatches = function(element) {
return findDropDown(element).find('li');
};
var triggerKeyDown = function(element, keyCode, options) {
options = options || {};
var inputEl = findInput(element);
var e = $.Event('keydown');
e.which = keyCode;
if (options.shiftKey) {
e.shiftKey = true;
}
inputEl.trigger(e);
};
//custom matchers
beforeEach(function () {
jasmine.addMatchers({
toBeClosed: function(util, customEqualityTesters) {
return {
compare: function(actual, expected) {
var typeaheadEl = findDropDown(actual);
var result = {
pass: util.equals(typeaheadEl.hasClass('ng-hide'), true, customEqualityTesters)
};
if (result.pass) {
result.message = 'Expected "' + angular.mock.dump(typeaheadEl) + '" not to be closed.';
} else {
result.message = 'Expected "' + angular.mock.dump(typeaheadEl) + '" to be closed.';
}
return result;
}
};
},
toBeOpenWithActive: function(util, customEqualityTesters) {
return {
compare: function(actual, noOfMatches, activeIdx) {
var typeaheadEl = findDropDown(actual);
var liEls = findMatches(actual);
var result = {
pass: util.equals(typeaheadEl.length, 1, customEqualityTesters) &&
util.equals(typeaheadEl.hasClass('ng-hide'), false, customEqualityTesters) &&
util.equals(liEls.length, noOfMatches, customEqualityTesters) &&
activeIdx === -1 ? !$(liEls).hasClass('active') : $(liEls[activeIdx]).hasClass('active')
};
if (result.pass) {
result.message = 'Expected "' + actual + '" not to be opened.';
} else {
result.message = 'Expected "' + actual + '" to be opened.';
}
return result;
}
};
}
});
});
afterEach(function() {
findDropDown($document.find('body')).remove();
});
//coarse grained, "integration" tests
describe('initial state and model changes', function() {
it('should be closed by default', function() {
var element = prepareInputEl('
');
expect(element).toBeClosed();
});
it('should correctly render initial state if the "as" keyword is used', function() {
$scope.result = $scope.states[0];
var element = prepareInputEl('
');
var inputEl = findInput(element);
expect(inputEl.val()).toEqual('Alaska');
});
it('should default to bound model for initial rendering if there is not enough info to render label', function() {
$scope.result = $scope.states[0].code;
var element = prepareInputEl('
');
var inputEl = findInput(element);
expect(inputEl.val()).toEqual('AL');
});
it('should not get open on model change', function() {
var element = prepareInputEl('
');
$scope.$apply(function () {
$scope.result = 'foo';
});
expect(element).toBeClosed();
});
});
describe('basic functionality', function() {
it('should open and close typeahead based on matches', function() {
var element = prepareInputEl('
');
var inputEl = findInput(element);
var ownsId = inputEl.attr('aria-owns');
expect(inputEl.attr('aria-expanded')).toBe('false');
expect(inputEl.attr('aria-activedescendant')).toBeUndefined();
changeInputValueTo(element, 'ba');
expect(element).toBeOpenWithActive(2, 0);
expect(findDropDown(element).attr('id')).toBe(ownsId);
expect(inputEl.attr('aria-expanded')).toBe('true');
var activeOptionId = ownsId + '-option-0';
expect(inputEl.attr('aria-activedescendant')).toBe(activeOptionId);
expect(findDropDown(element).find('li.active').attr('id')).toBe(activeOptionId);
changeInputValueTo(element, '');
expect(element).toBeClosed();
expect(inputEl.attr('aria-expanded')).toBe('false');
expect(inputEl.attr('aria-activedescendant')).toBeUndefined();
});
it('should allow expressions over multiple lines', function() {
var element = prepareInputEl('
');
changeInputValueTo(element, 'ba');
expect(element).toBeOpenWithActive(2, 0);
changeInputValueTo(element, '');
expect(element).toBeClosed();
});
it('should not open typeahead if input value smaller than a defined threshold', function() {
var element = prepareInputEl('
');
changeInputValueTo(element, 'b');
expect(element).toBeClosed();
});
it('should support changing min-length', function() {
$scope.typeAheadMinLength = 2;
var element = prepareInputEl('
');
changeInputValueTo(element, 'b');
expect(element).toBeClosed();
$scope.typeAheadMinLength = 0;
$scope.$digest();
changeInputValueTo(element, '');
expect(element).toBeOpenWithActive(3, 0);
$scope.typeAheadMinLength = 2;
$scope.$digest();
changeInputValueTo(element, 'b');
expect(element).toBeClosed();
});
it('should support custom model selecting function', function() {
$scope.updaterFn = function(selectedItem) {
return 'prefix' + selectedItem;
};
var element = prepareInputEl('
');
changeInputValueTo(element, 'f');
triggerKeyDown(element, 13);
expect($scope.result).toEqual('prefixfoo');
});
it('should support custom label rendering function', function() {
$scope.formatterFn = function(sourceItem) {
return 'prefix' + sourceItem;
};
var element = prepareInputEl('
');
changeInputValueTo(element, 'fo');
var matchHighlight = findMatches(element).find('a').html();
expect(matchHighlight).toEqual('prefix
foo');
});
it('should by default bind view value to model even if not part of matches', function() {
var element = prepareInputEl('
');
changeInputValueTo(element, 'not in matches');
expect($scope.result).toEqual('not in matches');
});
it('should support the editable property to limit model bindings to matches only', function() {
var element = prepareInputEl('
');
changeInputValueTo(element, 'not in matches');
expect($scope.result).toEqual(undefined);
});
it('should set validation errors for non-editable inputs', function() {
var element = prepareInputEl(
'
');
changeInputValueTo(element, 'not in matches');
expect($scope.result).toEqual(undefined);
expect($scope.form.input.$error.editable).toBeTruthy();
changeInputValueTo(element, 'foo');
triggerKeyDown(element, 13);
expect($scope.result).toEqual('foo');
expect($scope.form.input.$error.editable).toBeFalsy();
});
it('should not set editable validation error for empty input', function() {
var element = prepareInputEl(
'
');
changeInputValueTo(element, 'not in matches');
expect($scope.result).toEqual(undefined);
expect($scope.form.input.$error.editable).toBeTruthy();
changeInputValueTo(element, '');
expect($scope.result).toEqual(null);
expect($scope.form.input.$error.editable).toBeFalsy();
});
it('should clear view value after blur for typeahead-editable="false"', function () {
var element = prepareInputEl('
');
var inputEl = findInput(element);
changeInputValueTo(element, 'not in matches');
expect($scope.result).toEqual(undefined);
expect(inputEl.val()).toEqual('not in matches');
inputEl.blur(); // input loses focus
expect($scope.result).toEqual(undefined);
expect(inputEl.val()).toEqual('');
});
it('should clear errors after blur for typeahead-editable="false"', function () {
var element = prepareInputEl(
'
');
var inputEl = findInput(element);
changeInputValueTo(element, 'not in matches');
expect($scope.result).toEqual(undefined);
expect(inputEl.val()).toEqual('not in matches');
inputEl.blur();
expect($scope.form.input.$error.editable).toBeFalsy();
expect($scope.form.input.$error.parse).toBeFalsy();
});
// fix for #6032
it('should clear errors and refresh scope after blur for typeahead-editable="false"', function () {
var element = prepareInputEl(
'
');
var inputEl = findInput(element);
// first try
changeInputValueTo(element, 'not in matches');
expect($scope.result).toEqual(undefined);
expect(inputEl.val()).toEqual('not in matches');
expect(element.find('form')).toHaveClass('invalid');
inputEl.blur();
expect(inputEl.val()).toEqual(''); // <-- input is reset
expect($scope.form.input.$error.editable).toBeFalsy();
expect($scope.form.input.$error.parse).toBeFalsy();
expect(element.find('form')).not.toHaveClass('invalid'); // <-- form has no error (it always works for some reason)
// second try
changeInputValueTo(element, 'not in matches');
expect($scope.result).toEqual(undefined);
expect(inputEl.val()).toEqual('not in matches');
expect(element.find('form')).toHaveClass('invalid');
inputEl.blur();
expect(inputEl.val()).toEqual(''); // <-- input is reset
expect($scope.form.input.$error.editable).toBeFalsy();
expect($scope.form.input.$error.parse).toBeFalsy();
expect(element.find('form')).not.toHaveClass('invalid'); // <-- form has no error (it didn't work prior to #6032 fix)
});
it('should go through other validators after blur for typeahead-editable="false"', function () {
var element = prepareInputEl(
'
');
var inputEl = findInput(element);
changeInputValueTo(element, 'not in matches');
expect($scope.result).toEqual(undefined);
expect(inputEl.val()).toEqual('not in matches');
inputEl.blur(); // input loses focus
expect($scope.result).toEqual(undefined);
expect($scope.form.input.$error.required).toBeTruthy();
});
it('should clear view value when no value selected for typeahead-editable="false" typeahead-select-on-blur="false"', function () {
var element = prepareInputEl('
');
var inputEl = findInput(element);
changeInputValueTo(element, 'b');
expect($scope.result).toEqual(undefined);
expect(inputEl.val()).toEqual('b');
inputEl.blur(); // input loses focus
expect($scope.result).toEqual(undefined);
expect(inputEl.val()).toEqual('');
});
it('should not clear view value when there is match but no value selected for typeahead-editable="false" typeahead-select-on-blur="true"', function () {
var element = prepareInputEl('
');
var inputEl = findInput(element);
changeInputValueTo(element, 'b');
expect($scope.result).toEqual(undefined);
expect(inputEl.val()).toEqual('b');
inputEl.blur(); // input loses focus
expect($scope.result).toEqual('bar');
expect(inputEl.val()).toEqual('bar');
});
it('should support changing the editable property to limit model bindings to matches only', function() {
$scope.isEditable = true;
var element = prepareInputEl('
');
$scope.isEditable = false;
$scope.$digest();
changeInputValueTo(element, 'not in matches');
expect($scope.result).toEqual(undefined);
});
it('should support changing the editable property to bind view value to model even if not part of matches', function() {
$scope.isEditable = false;
var element = prepareInputEl('
');
$scope.isEditable = true;
$scope.$digest();
changeInputValueTo(element, 'not in matches');
expect($scope.result).toEqual('not in matches');
});
it('should bind loading indicator expression', inject(function($timeout) {
$scope.isLoading = false;
$scope.loadMatches = function(viewValue) {
return $timeout(function() {
return [];
}, 1000);
};
var element = prepareInputEl('
');
changeInputValueTo(element, 'foo');
expect($scope.isLoading).toBeTruthy();
$timeout.flush();
expect($scope.isLoading).toBeFalsy();
}));
it('should support timeout before trying to match $viewValue', inject(function($timeout) {
var element = prepareInputEl('
');
changeInputValueTo(element, 'foo');
expect(element).toBeClosed();
$timeout.flush();
expect(element).toBeOpenWithActive(1, 0);
}));
it('should cancel old timeouts when something is typed within waitTime', inject(function($timeout) {
var values = [];
$scope.loadMatches = function(viewValue) {
values.push(viewValue);
return $scope.source;
};
var element = prepareInputEl('
');
changeInputValueTo(element, 'first');
changeInputValueTo(element, 'second');
$timeout.flush();
expect(values).not.toContain('first');
}));
it('should allow timeouts when something is typed after waitTime has passed', inject(function($timeout) {
var values = [];
$scope.loadMatches = function(viewValue) {
values.push(viewValue);
return $scope.source;
};
var element = prepareInputEl('
');
changeInputValueTo(element, 'first');
$timeout.flush();
expect(values).toContain('first');
changeInputValueTo(element, 'second');
$timeout.flush();
expect(values).toContain('second');
}));
it('should support custom popup templates', function() {
$templateCache.put('custom.html', '
foo
');
var element = prepareInputEl('
');
changeInputValueTo(element, 'Al');
expect(element.find('.custom').text()).toBe('foo');
});
it('should support custom templates for matched items', function() {
$templateCache.put('custom.html', '
{{ index }} {{ match.label }}
');
var element = prepareInputEl('
');
changeInputValueTo(element, 'Al');
expect(findMatches(element).eq(0).find('p').text()).toEqual('0 Alaska');
});
it('should support directives which require controllers in custom templates for matched items', function() {
$templateCache.put('custom.html', '
{{ index }} {{ match.label }}
');
var element = prepareInputEl('
');
element.data('$parentDirectiveController', {});
changeInputValueTo(element, 'Al');
expect(findMatches(element).eq(0).find('p').text()).toEqual('0 Alaska');
});
it('should throw error on invalid expression', function() {
var prepareInvalidDir = function() {
prepareInputEl('
');
};
expect(prepareInvalidDir).toThrow();
});
it('should remove the id attribute from the original DOM element', function() {
var element = prepareInputEl('
');
var inputEl = findInput(element);
expect(inputEl.size()).toBe(2);
expect(inputEl.eq(0).attr('id')).toBe(undefined);
expect(inputEl.eq(1).attr('id')).toBe('typeahead-element');
});
});
describe('shouldSelect', function() {
it('should select a match when function returns true', function() {
$scope.shouldSelectFn = function() {
return true;
};
var element = prepareInputEl('
');
var inputEl = findInput(element);
changeInputValueTo(element, 'b');
triggerKeyDown(element, 13);
expect($scope.result).toEqual('bar');
expect(inputEl.val()).toEqual('bar');
expect(element).toBeClosed();
});
it('should not select a match when function returns false', function() {
$scope.shouldSelectFn = function() {
return false;
};
var element = prepareInputEl('
');
var inputEl = findInput(element);
changeInputValueTo(element, 'b');
triggerKeyDown(element, 13);
// no change
expect($scope.result).toEqual('b');
expect(inputEl.val()).toEqual('b');
});
it('should pass key event into select trigger function', function() {
$scope.shouldSelectFn = jasmine.createSpy('shouldSelectFn');//.and.returnValue(true);
var element = prepareInputEl('
');
var inputEl = findInput(element);
changeInputValueTo(element, 'b');
triggerKeyDown(element, 13);
expect($scope.shouldSelectFn.calls.count()).toEqual(1);
expect($scope.shouldSelectFn.calls.argsFor(0)[0].which).toEqual(13);
});
});
describe('selecting a match', function() {
it('should select a match on enter', function() {
var element = prepareInputEl('
');
var inputEl = findInput(element);
changeInputValueTo(element, 'b');
triggerKeyDown(element, 13);
expect($scope.result).toEqual('bar');
expect(inputEl.val()).toEqual('bar');
expect(element).toBeClosed();
});
it('should select a match on tab', function() {
var element = prepareInputEl('
');
var inputEl = findInput(element);
changeInputValueTo(element, 'b');
triggerKeyDown(element, 9);
expect($scope.result).toEqual('bar');
expect(inputEl.val()).toEqual('bar');
expect(element).toBeClosed();
});
it('should not select any match on blur without \'select-on-blur=true\' option', function() {
var element = prepareInputEl('
');
var inputEl = findInput(element);
changeInputValueTo(element, 'b');
inputEl.blur(); // input loses focus
// no change
expect($scope.result).toEqual('b');
expect(inputEl.val()).toEqual('b');
});
it('should select a match on blur with \'select-on-blur=true\' option', function() {
var element = prepareInputEl('
');
var inputEl = findInput(element);
changeInputValueTo(element, 'b');
inputEl.blur(); // input loses focus
// first element should be selected
expect($scope.result).toEqual('bar');
expect(inputEl.val()).toEqual('bar');
});
it('should select match on click', function() {
var element = prepareInputEl('
');
var inputEl = findInput(element);
changeInputValueTo(element, 'b');
var match = $(findMatches(element)[1]).find('a')[0];
$(match).click();
$scope.$digest();
expect($scope.result).toEqual('baz');
expect(inputEl.val()).toEqual('baz');
expect(element).toBeClosed();
});
it('should invoke select callback on select', function() {
$scope.onSelect = function($item, $model, $label, $event) {
$scope.$item = $item;
$scope.$model = $model;
$scope.$label = $label;
$scope.$event = $event;
};
var element = prepareInputEl('
');
changeInputValueTo(element, 'Alas');
triggerKeyDown(element, 13);
expect($scope.result).toEqual('AL');
expect($scope.$item).toEqual($scope.states[0]);
expect($scope.$model).toEqual('AL');
expect($scope.$label).toEqual('Alaska');
expect($scope.$event.type).toEqual("keydown");
});
it('should correctly update inputs value on mapping where label is not derived from the model', function() {
var element = prepareInputEl('
');
var inputEl = findInput(element);
changeInputValueTo(element, 'Alas');
triggerKeyDown(element, 13);
expect($scope.result).toEqual('AL');
expect(inputEl.val()).toEqual('AL');
});
it('should bind no results indicator as true when no matches returned', inject(function($timeout) {
$scope.isNoResults = false;
$scope.loadMatches = function(viewValue) {
return $timeout(function() {
return [];
}, 1000);
};
var element = prepareInputEl('
');
changeInputValueTo(element, 'foo');
expect($scope.isNoResults).toBeFalsy();
$timeout.flush();
expect($scope.isNoResults).toBeTruthy();
}));
it('should bind no results indicator as false when matches are returned', inject(function($timeout) {
$scope.isNoResults = false;
$scope.loadMatches = function(viewValue) {
return $timeout(function() {
return [viewValue];
}, 1000);
};
var element = prepareInputEl('
');
changeInputValueTo(element, 'foo');
expect($scope.isNoResults).toBeFalsy();
$timeout.flush();
expect($scope.isNoResults).toBeFalsy();
}));
it('should not focus the input if `typeahead-focus-on-select` is false', function() {
var element = prepareInputEl('
');
$document.find('body').append(element);
var inputEl = findInput(element);
changeInputValueTo(element, 'b');
var match = $(findMatches(element)[1]).find('a')[0];
$(match).click();
$scope.$digest();
$timeout.flush();
expect(document.activeElement).not.toBe(inputEl[0]);
expect($scope.result).toEqual('baz');
});
});
describe('select on exact match', function() {
it('should select on an exact match when set', function() {
$scope.onSelect = jasmine.createSpy('onSelect');
var element = prepareInputEl('
');
var inputEl = findInput(element);
changeInputValueTo(element, 'bar');
expect($scope.result).toEqual('bar');
expect(inputEl.val()).toEqual('bar');
expect(element).toBeClosed();
expect($scope.onSelect).toHaveBeenCalled();
});
it('should not select on an exact match by default', function() {
$scope.onSelect = jasmine.createSpy('onSelect');
var element = prepareInputEl('
');
var inputEl = findInput(element);
changeInputValueTo(element, 'bar');
expect($scope.result).toBeUndefined();
expect(inputEl.val()).toEqual('bar');
expect($scope.onSelect.calls.any()).toBe(false);
});
it('should not be case sensitive when select on an exact match', function() {
$scope.onSelect = jasmine.createSpy('onSelect');
var element = prepareInputEl('
');
var inputEl = findInput(element);
changeInputValueTo(element, 'BaR');
expect($scope.result).toEqual('bar');
expect(inputEl.val()).toEqual('bar');
expect(element).toBeClosed();
expect($scope.onSelect).toHaveBeenCalled();
});
it('should not auto select when not a match with one potential result left', function() {
$scope.onSelect = jasmine.createSpy('onSelect');
var element = prepareInputEl('
');
var inputEl = findInput(element);
changeInputValueTo(element, 'fo');
expect($scope.result).toBeUndefined();
expect(inputEl.val()).toEqual('fo');
expect($scope.onSelect.calls.any()).toBe(false);
});
});
describe('is-open indicator', function () {
var element;
beforeEach(function () {
element = prepareInputEl('
');
});
it('should bind is-open indicator as true when matches are returned', function () {
expect($scope.isOpen).toBeFalsy();
changeInputValueTo(element, 'b');
expect($scope.isOpen).toBeTruthy();
});
it('should bind is-open indicator as false when no matches returned', function () {
expect($scope.isOpen).toBeFalsy();
changeInputValueTo(element, 'b');
expect($scope.isOpen).toBeTruthy();
changeInputValueTo(element, 'not match');
expect($scope.isOpen).toBeFalsy();
});
it('should bind is-open indicator as false when a match is clicked', function () {
expect($scope.isOpen).toBeFalsy();
changeInputValueTo(element, 'b');
expect($scope.isOpen).toBeTruthy();
var match = findMatches(element).find('a').eq(0);
match.click();
$scope.$digest();
expect($scope.isOpen).toBeFalsy();
});
it('should bind is-open indicator as false when click outside', function () {
expect($scope.isOpen).toBeFalsy();
changeInputValueTo(element, 'b');
expect($scope.isOpen).toBeTruthy();
$document.find('body').click();
$scope.$digest();
expect($scope.isOpen).toBeFalsy();
});
it('should bind is-open indicator as false on enter', function () {
expect($scope.isOpen).toBeFalsy();
changeInputValueTo(element, 'b');
expect($scope.isOpen).toBeTruthy();
triggerKeyDown(element, 13);
expect($scope.isOpen).toBeFalsy();
});
it('should bind is-open indicator as false on tab', function () {
expect($scope.isOpen).toBeFalsy();
changeInputValueTo(element, 'b');
expect($scope.isOpen).toBeTruthy();
triggerKeyDown(element, 9);
expect($scope.isOpen).toBeFalsy();
});
it('should bind is-open indicator as false on escape key', function () {
expect($scope.isOpen).toBeFalsy();
changeInputValueTo(element, 'b');
expect($scope.isOpen).toBeTruthy();
triggerKeyDown(element, 27);
expect($scope.isOpen).toBeFalsy();
});
it('should bind is-open indicator as false input value smaller than a defined threshold', function () {
var element = prepareInputEl('
');
expect($scope.isToggled).toBeFalsy();
changeInputValueTo(element, 'b');
expect($scope.isToggled).toBeFalsy();
});
});
describe('pop-up interaction', function() {
var element;
beforeEach(function() {
element = prepareInputEl('
');
});
it('should activate prev/next matches on up/down keys', function() {
changeInputValueTo(element, 'b');
var parentNode = element.find('ul').eq(0)[0];
var liIndex;
liIndex = 0;
expect(element).toBeOpenWithActive(2, liIndex);
expect(parentNode.scrollTop).toEqual(element.find('li').eq(liIndex)[0].offsetTop);
// Down arrow key
triggerKeyDown(element, 40);
liIndex = 1;
expect(element).toBeOpenWithActive(2, liIndex);
expect(parentNode.scrollTop).toEqual(element.find('li').eq(liIndex)[0].offsetTop);
// Down arrow key goes back to first element
triggerKeyDown(element, 40);
liIndex = 0;
expect(element).toBeOpenWithActive(2, liIndex);
expect(parentNode.scrollTop).toEqual(element.find('li').eq(liIndex)[0].offsetTop);
// Up arrow key goes back to last element
triggerKeyDown(element, 38);
liIndex = 1;
expect(element).toBeOpenWithActive(2, liIndex);
expect(parentNode.scrollTop).toEqual(element.find('li').eq(liIndex)[0].offsetTop);
// Up arrow key goes back to first element
triggerKeyDown(element, 38);
liIndex = 0;
expect(parentNode.scrollTop).toEqual(element.find('li').eq(liIndex)[0].offsetTop);
expect(element).toBeOpenWithActive(2, liIndex);
});
it('should close popup on escape key', function() {
changeInputValueTo(element, 'b');
expect(element).toBeOpenWithActive(2, 0);
// Escape key
triggerKeyDown(element, 27);
expect(element).toBeClosed();
});
it('should highlight match on mouseenter', function() {
changeInputValueTo(element, 'b');
expect(element).toBeOpenWithActive(2, 0);
findMatches(element).eq(1).trigger('mouseenter');
expect(element).toBeOpenWithActive(2, 1);
});
});
describe('promises', function() {
var element, deferred;
beforeEach(inject(function($q) {
deferred = $q.defer();
$scope.source = function() {
return deferred.promise;
};
element = prepareInputEl('
');
}));
it('should display matches from promise', function() {
changeInputValueTo(element, 'c');
expect(element).toBeClosed();
deferred.resolve(['good', 'stuff']);
$scope.$digest();
expect(element).toBeOpenWithActive(2, 0);
});
it('should not display anything when promise is rejected', function() {
changeInputValueTo(element, 'c');
expect(element).toBeClosed();
deferred.reject('fail');
$scope.$digest();
expect(element).toBeClosed();
});
it('PR #3178, resolves #2999 - should not return property "length" of undefined for undefined matches', function() {
changeInputValueTo(element, 'c');
expect(element).toBeClosed();
deferred.resolve();
$scope.$digest();
expect(element).toBeClosed();
});
});
describe('non-regressions tests', function() {
it('issue 231 - closes matches popup on click outside typeahead', function() {
var element = prepareInputEl('
');
changeInputValueTo(element, 'b');
$document.find('body').click();
$scope.$digest();
expect(element).toBeClosed();
});
it('issue 591 - initial formatting for un-selected match and complex label expression', function() {
var inputEl = findInput(prepareInputEl('
'));
expect(inputEl.val()).toEqual('');
});
it('issue 786 - name of internal model should not conflict with scope model name', function() {
$scope.state = $scope.states[0];
var element = prepareInputEl('
');
var inputEl = findInput(element);
expect(inputEl.val()).toEqual('Alaska');
});
it('issue 863 - it should work correctly with input type="email"', function() {
$scope.emails = ['foo@host.com', 'bar@host.com'];
var element = prepareInputEl('
');
var inputEl = findInput(element);
changeInputValueTo(element, 'bar');
expect(element).toBeOpenWithActive(1, 0);
triggerKeyDown(element, 13);
expect($scope.email).toEqual('bar@host.com');
expect(inputEl.val()).toEqual('bar@host.com');
});
it('issue 964 - should not show popup with matches if an element is not focused', function() {
$scope.items = function(viewValue) {
return $timeout(function() {
return [viewValue];
});
};
var element = prepareInputEl('
');
var inputEl = findInput(element);
changeInputValueTo(element, 'match');
$scope.$digest();
inputEl.blur();
$timeout.flush();
expect(element).toBeClosed();
});
it('should properly update loading callback if an element is not focused', function() {
$scope.items = function(viewValue) {
return $timeout(function(){
return [viewValue];
});
};
var element = prepareInputEl('
');
var inputEl = findInput(element);
changeInputValueTo(element, 'match');
$scope.$digest();
inputEl.blur();
$timeout.flush();
expect($scope.isLoading).toBeFalsy();
});
it('issue 1140 - should properly update loading callback when deleting characters', function() {
$scope.items = function(viewValue) {
return $timeout(function() {
return [viewValue];
});
};
var element = prepareInputEl('
');
changeInputValueTo(element, 'match');
$scope.$digest();
expect($scope.isLoading).toBeTruthy();
changeInputValueTo(element, 'm');
$timeout.flush();
$scope.$digest();
expect($scope.isLoading).toBeFalsy();
});
it('should cancel old timeout when deleting characters', inject(function($timeout) {
var values = [];
$scope.loadMatches = function(viewValue) {
values.push(viewValue);
return $scope.source;
};
var element = prepareInputEl('
');
changeInputValueTo(element, 'match');
changeInputValueTo(element, 'm');
$timeout.flush();
expect(values).not.toContain('match');
}));
describe('', function() {
// Dummy describe to be able to create an after hook for this tests
var element;
it('does not close matches popup on click in input', function() {
element = prepareInputEl('
');
var inputEl = findInput(element);
// Note that this bug can only be found when element is in the document
$document.find('body').append(element);
changeInputValueTo(element, 'b');
inputEl.click();
$scope.$digest();
expect(element).toBeOpenWithActive(2, 0);
});
it('issue #1773 - should not trigger an error when used with ng-focus', function() {
element = prepareInputEl('
');
var inputEl = findInput(element);
// Note that this bug can only be found when element is in the document
$document.find('body').append(element);
changeInputValueTo(element, 'b');
var match = $(findMatches(element)[1]).find('a')[0];
$(match).click();
$scope.$digest();
});
afterEach(function() {
element.remove();
});
});
it('issue #1238 - allow names like "query" to be used inside "in" expressions ', function() {
$scope.query = function() {
return ['foo', 'bar'];
};
var element = prepareInputEl('
');
changeInputValueTo(element, 'bar');
expect(element).toBeOpenWithActive(2, 0);
});
it('issue #3318 - should set model validity to true when set manually', function() {
var element = prepareInputEl(
'
');
changeInputValueTo(element, 'not in matches');
$scope.$apply(function() {
$scope.result = 'manually set';
});
expect($scope.result).toEqual('manually set');
expect($scope.form.input.$valid).toBeTruthy();
});
it('issue #3166 - should set \'parse\' key as valid when selecting a perfect match and not editable', function() {
var element = prepareInputEl('
');
var inputEl = findInput(element);
changeInputValueTo(element, 'Alaska');
triggerKeyDown(element, 13);
expect($scope.test.typeahead.$error.parse).toBeUndefined();
});
});
describe('ng-model-options', function() {
it('should support getterSetter', function() {
function resultSetter(state) {
return state;
}
$scope.result = resultSetter;
var element = prepareInputEl('
');
changeInputValueTo(element, 'Alaska');
triggerKeyDown(element, 13);
expect($scope.result).toBe(resultSetter);
});
describe('debounce as a number', function() {
it('should work with selecting via keyboard', function() {
element = prepareInputEl('
');
var inputEl = findInput(element);
changeInputValueTo(element, 'Alaska');
triggerKeyDown(element, 13);
expect($scope.result).not.toBe('Alaska');
$timeout.flush(400);
expect($scope.result).toBe('Alaska');
});
it('should work with select on exact', function() {
element = prepareInputEl('
');
var inputEl = findInput(element);
changeInputValueTo(element, 'Alaska');
expect($scope.result).not.toBe('Alaska');
$timeout.flush(400);
expect($scope.result).toBe('Alaska');
});
it('should work with selecting a match via click', function() {
element = prepareInputEl('
');
var inputEl = findInput(element);
changeInputValueTo(element, 'Alaska');
var match = $(findMatches(element)[0]).find('a')[0];
$(match).click();
$scope.$digest();
expect($scope.result).not.toBe('Alaska');
$timeout.flush(400);
expect($scope.result).toBe('Alaska');
});
});
describe('debounce as an object', function() {
it('should work with selecting via keyboard', function() {
element = prepareInputEl('
');
var inputEl = findInput(element);
changeInputValueTo(element, 'Alaska');
triggerKeyDown(element, 13);
expect($scope.result).not.toBe('Alaska');
$timeout.flush(400);
expect($scope.result).toBe('Alaska');
});
it('should work with select on exact', function() {
element = prepareInputEl('
');
var inputEl = findInput(element);
changeInputValueTo(element, 'Alaska');
expect($scope.result).not.toBe('Alaska');
$timeout.flush(400);
expect($scope.result).toBe('Alaska');
});
it('should work with selecting a match via click', function() {
element = prepareInputEl('
');
var inputEl = findInput(element);
changeInputValueTo(element, 'Alaska');
var match = $(findMatches(element)[0]).find('a')[0];
$(match).click();
$scope.$digest();
expect($scope.result).not.toBe('Alaska');
$timeout.flush(400);
expect($scope.result).toBe('Alaska');
});
it('should work when blurring and select on blur', function() {
element = prepareInputEl('
');
var inputEl = findInput(element);
changeInputValueTo(element, 'Alaska');
element.blur();
$scope.$digest();
expect($scope.result).not.toBe('Alaska');
$timeout.flush(500);
expect($scope.result).toBe('Alaska');
});
});
});
describe('input formatting', function() {
it('should co-operate with existing formatters', function() {
$scope.result = $scope.states[0];
var element = prepareInputEl('
'),
inputEl = findInput(element);
expect(inputEl.val()).toEqual('formatted' + $scope.result.name);
});
it('should support a custom input formatting function', function() {
$scope.result = $scope.states[0];
$scope.formatInput = function($model) {
return $model.code;
};
var element = prepareInputEl('
'),
inputEl = findInput(element);
expect(inputEl.val()).toEqual('AL');
expect($scope.result).toEqual($scope.states[0]);
});
});
describe('input hint', function() {
var element;
beforeEach(function() {
element = prepareInputEl('
');
});
it('should show hint when input matches first match', function() {
var hintEl = findInput(element);
expect(hintEl.val()).toEqual('');
changeInputValueTo(element, 'Alas');
expect(hintEl.val()).toEqual('Alaska');
});
it('should not show hint when input does not match first match', function() {
var hintEl = findInput(element);
expect(hintEl.val()).toEqual('');
changeInputValueTo(element, 'las');
expect(hintEl.val()).toEqual('');
});
it('should reset hint when a match is clicked', function() {
var hintEl = findInput(element);
expect(hintEl.val()).toEqual('');
changeInputValueTo(element, 'Alas');
expect(hintEl.val()).toEqual('Alaska');
var match = findMatches(element).find('a').eq(0);
match.click();
$scope.$digest();
expect(hintEl.val()).toEqual('');
});
it('should reset hint when click outside', function() {
var hintEl = findInput(element);
expect(hintEl.val()).toEqual('');
changeInputValueTo(element, 'Alas');
expect(hintEl.val()).toEqual('Alaska');
$document.find('body').click();
$scope.$digest();
expect(hintEl.val()).toEqual('');
});
it('should reset hint on enter', function() {
var hintEl = findInput(element);
expect(hintEl.val()).toEqual('');
changeInputValueTo(element, 'Alas');
expect(hintEl.val()).toEqual('Alaska');
triggerKeyDown(element, 13);
expect(hintEl.val()).toEqual('');
});
it('should reset hint on tab', function() {
var hintEl = findInput(element);
expect(hintEl.val()).toEqual('');
changeInputValueTo(element, 'Alas');
expect(hintEl.val()).toEqual('Alaska');
triggerKeyDown(element, 9);
expect(hintEl.val()).toEqual('');
});
it('should reset hint on escape key', function() {
var hintEl = findInput(element);
expect(hintEl.val()).toEqual('');
changeInputValueTo(element, 'Alas');
expect(hintEl.val()).toEqual('Alaska');
triggerKeyDown(element, 27);
expect(hintEl.val()).toEqual('');
});
it("should set tab index on hint input element", function(){
var hintEl = findInput(element);
expect(hintEl.attr('tabindex')).toEqual('-1');
});
});
describe('append to', function() {
it('append typeahead results to element', function() {
$document.find('body').append('
');
$scope.myElement = $document.find('#myElement');
var element = prepareInputEl('
');
changeInputValueTo(element, 'al');
expect($document.find('#myElement')).toBeOpenWithActive(2, 0);
$document.find('#myElement').remove();
});
});
describe('append to body', function() {
afterEach(function() {
angular.element($window).off('resize');
$document.find('body').off('scroll');
});
it('append typeahead results to body', function() {
var element = prepareInputEl('
');
changeInputValueTo(element, 'ba');
expect($document.find('body')).toBeOpenWithActive(2, 0);
});
it('should not append to body when value of the attribute is false', function() {
var element = prepareInputEl('
');
changeInputValueTo(element, 'ba');
expect(findDropDown($document.find('body')).length).toEqual(0);
});
it('should have right position after scroll', function() {
var element = prepareInputEl('
');
var dropdown = findDropDown($document.find('body'));
var body = angular.element(document.body);
// Set body height to allow scrolling
body.css({height:'10000px'});
// Scroll top
window.scroll(0, 1000);
// Set input value to show dropdown
changeInputValueTo(element, 'ba');
// Init position of dropdown must be 1000px
expect(dropdown.css('top') ).toEqual('1000px');
// After scroll, must have new position
window.scroll(0, 500);
body.triggerHandler('scroll');
$timeout.flush();
expect(dropdown.css('top')).toEqual('500px');
});
});
describe('focus first', function() {
it('should focus the first element by default', function() {
var element = prepareInputEl('
');
changeInputValueTo(element, 'b');
expect(element).toBeOpenWithActive(2, 0);
// Down arrow key
triggerKeyDown(element, 40);
expect(element).toBeOpenWithActive(2, 1);
// Down arrow key goes back to first element
triggerKeyDown(element, 40);
expect(element).toBeOpenWithActive(2, 0);
// Up arrow key goes back to last element
triggerKeyDown(element, 38);
expect(element).toBeOpenWithActive(2, 1);
// Up arrow key goes back to first element
triggerKeyDown(element, 38);
expect(element).toBeOpenWithActive(2, 0);
});
it('should not focus the first element until keys are pressed', function() {
var element = prepareInputEl('
');
changeInputValueTo(element, 'b');
expect(element).toBeOpenWithActive(2, -1);
// Down arrow key goes to first element
triggerKeyDown(element, 40);
expect(element).toBeOpenWithActive(2, 0);
// Down arrow key goes to second element
triggerKeyDown(element, 40);
expect(element).toBeOpenWithActive(2, 1);
// Down arrow key goes back to first element
triggerKeyDown(element, 40);
expect(element).toBeOpenWithActive(2, 0);
// Up arrow key goes back to last element
triggerKeyDown(element, 38);
expect(element).toBeOpenWithActive(2, 1);
// Up arrow key goes back to first element
triggerKeyDown(element, 38);
expect(element).toBeOpenWithActive(2, 0);
// New input goes back to no focus
changeInputValueTo(element, 'a');
changeInputValueTo(element, 'b');
expect(element).toBeOpenWithActive(2, -1);
// Up arrow key goes to last element
triggerKeyDown(element, 38);
expect(element).toBeOpenWithActive(2, 1);
});
});
it('should not capture enter or tab when an item is not focused', function() {
$scope.select_count = 0;
$scope.onSelect = function($item, $model, $label) {
$scope.select_count = $scope.select_count + 1;
};
var element = prepareInputEl('
');
changeInputValueTo(element, 'b');
// enter key should not be captured when nothing is focused
triggerKeyDown(element, 13);
expect($scope.keyDownEvent.isDefaultPrevented()).toBeFalsy();
expect($scope.select_count).toEqual(0);
// tab key should close the dropdown when nothing is focused
triggerKeyDown(element, 9);
expect($scope.keyDownEvent.isDefaultPrevented()).toBeFalsy();
expect($scope.select_count).toEqual(0);
expect(element).toBeClosed();
});
it("should not capture tab when shift key is pressed", function(){
$scope.select_count = 0;
$scope.onSelect = function($item, $model, $label) {
$scope.select_count = $scope.select_count + 1;
};
var element = prepareInputEl('
');
changeInputValueTo(element, 'b');
// down key should be captured and focus first element
triggerKeyDown(element, 40);
triggerKeyDown(element, 9, {shiftKey: true});
expect($scope.keyDownEvent.isDefaultPrevented()).toBeFalsy();
expect($scope.select_count).toEqual(0);
expect(element).toBeClosed();
});
it('should capture enter or tab when an item is focused', function() {
$scope.select_count = 0;
$scope.onSelect = function($item, $model, $label) {
$scope.select_count = $scope.select_count + 1;
};
var element = prepareInputEl('
');
changeInputValueTo(element, 'b');
// down key should be captured and focus first element
triggerKeyDown(element, 40);
expect($scope.keyDownEvent.isDefaultPrevented()).toBeTruthy();
expect(element).toBeOpenWithActive(2, 0);
// enter key should be captured now that something is focused
triggerKeyDown(element, 13);
expect($scope.keyDownEvent.isDefaultPrevented()).toBeTruthy();
expect($scope.select_count).toEqual(1);
});
describe('minLength set to 0', function() {
it('should open typeahead if input is changed to empty string if defined threshold is 0', function() {
var element = prepareInputEl('
');
changeInputValueTo(element, '');
expect(element).toBeOpenWithActive(3, 0);
});
it('should open typeahead when input is focused and value is empty if defined threshold is 0', function () {
var element = prepareInputEl('
');
var inputEl = findInput(element);
inputEl.focus();
$timeout.flush();
$scope.$digest();
expect(element).toBeOpenWithActive(3, 0);
});
});
describe('event listeners', function() {
afterEach(function() {
angular.element($window).off('resize');
$document.find('body').off('scroll');
});
it('should register event listeners when attached to body', function() {
spyOn(window, 'addEventListener');
spyOn(document.body, 'addEventListener');
var element = prepareInputEl('
');
expect(window.addEventListener).toHaveBeenCalledWith('resize', jasmine.any(Function), false);
expect(document.body.addEventListener).toHaveBeenCalledWith('scroll', jasmine.any(Function), false);
});
it('should remove event listeners when attached to body', function() {
spyOn(window, 'removeEventListener');
spyOn(document.body, 'removeEventListener');
var element = prepareInputEl('
');
$scope.$destroy();
expect(window.removeEventListener).toHaveBeenCalledWith('resize', jasmine.any(Function), false);
expect(document.body.removeEventListener).toHaveBeenCalledWith('scroll', jasmine.any(Function), false);
});
});
});
describe('typeahead tests', function() {
it('should allow directives in template to require parent controller', function() {
module('ui.bootstrap.typeahead');
module('ngSanitize');
module('uib/template/typeahead/typeahead-popup.html');
module(function($compileProvider) {
$compileProvider
.directive('uibCustomParent', function() {
return {
controller: function() {
this.text = 'foo';
}
};
})
.directive('uibCustomDirective', function() {
return {
require: '^uibCustomParent',
link: function(scope, element, attrs, ctrl) {
scope.text = ctrl.text;
}
};
});
});
inject(function($compile, $rootScope, $sniffer, $templateCache) {
var element;
var $scope = $rootScope.$new();
$templateCache.put('uib/template/typeahead/typeahead-match.html', '
{{text}}
');
$scope.states = [
{code: 'AL', name: 'Alaska'},
{code: 'CL', name: 'California'}
];
element = $compile('
')($scope);
$rootScope.$digest();
var inputEl = element.find('input');
inputEl.val('Al');
inputEl.trigger($sniffer.hasEvent('input') ? 'input' : 'change');
$scope.$digest();
expect(element.find('ul.dropdown-menu li').eq(0).find('[uib-custom-directive]').text()).toEqual('foo');
});
});
});
================================================
FILE: src/typeahead/typeahead.css
================================================
[uib-typeahead-popup].dropdown-menu {
display: block;
}
================================================
FILE: src/typeahead/typeahead.js
================================================
angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.debounce', 'ui.bootstrap.position'])
/**
* A helper service that can parse typeahead's syntax (string provided by users)
* Extracted to a separate service for ease of unit testing
*/
.factory('uibTypeaheadParser', ['$parse', function($parse) {
// 000001111111100000000000002222222200000000000000003333333333333330000000000044444444000
var TYPEAHEAD_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+([\s\S]+?)$/;
return {
parse: function(input) {
var match = input.match(TYPEAHEAD_REGEXP);
if (!match) {
throw new Error(
'Expected typeahead specification in form of "_modelValue_ (as _label_)? for _item_ in _collection_"' +
' but got "' + input + '".');
}
return {
itemName: match[3],
source: $parse(match[4]),
viewMapper: $parse(match[2] || match[1]),
modelMapper: $parse(match[1])
};
}
};
}])
.controller('UibTypeaheadController', ['$scope', '$element', '$attrs', '$compile', '$parse', '$q', '$timeout', '$document', '$window', '$rootScope', '$$debounce', '$uibPosition', 'uibTypeaheadParser',
function(originalScope, element, attrs, $compile, $parse, $q, $timeout, $document, $window, $rootScope, $$debounce, $position, typeaheadParser) {
var HOT_KEYS = [9, 13, 27, 38, 40];
var eventDebounceTime = 200;
var modelCtrl, ngModelOptions;
//SUPPORTED ATTRIBUTES (OPTIONS)
//minimal no of characters that needs to be entered before typeahead kicks-in
var minLength = originalScope.$eval(attrs.typeaheadMinLength);
if (!minLength && minLength !== 0) {
minLength = 1;
}
originalScope.$watch(attrs.typeaheadMinLength, function (newVal) {
minLength = !newVal && newVal !== 0 ? 1 : newVal;
});
//minimal wait time after last character typed before typeahead kicks-in
var waitTime = originalScope.$eval(attrs.typeaheadWaitMs) || 0;
//should it restrict model values to the ones selected from the popup only?
var isEditable = originalScope.$eval(attrs.typeaheadEditable) !== false;
originalScope.$watch(attrs.typeaheadEditable, function (newVal) {
isEditable = newVal !== false;
});
//binding to a variable that indicates if matches are being retrieved asynchronously
var isLoadingSetter = $parse(attrs.typeaheadLoading).assign || angular.noop;
//a function to determine if an event should cause selection
var isSelectEvent = attrs.typeaheadShouldSelect ? $parse(attrs.typeaheadShouldSelect) : function(scope, vals) {
var evt = vals.$event;
return evt.which === 13 || evt.which === 9;
};
//a callback executed when a match is selected
var onSelectCallback = $parse(attrs.typeaheadOnSelect);
//should it select highlighted popup value when losing focus?
var isSelectOnBlur = angular.isDefined(attrs.typeaheadSelectOnBlur) ? originalScope.$eval(attrs.typeaheadSelectOnBlur) : false;
//binding to a variable that indicates if there were no results after the query is completed
var isNoResultsSetter = $parse(attrs.typeaheadNoResults).assign || angular.noop;
var inputFormatter = attrs.typeaheadInputFormatter ? $parse(attrs.typeaheadInputFormatter) : undefined;
var appendToBody = attrs.typeaheadAppendToBody ? originalScope.$eval(attrs.typeaheadAppendToBody) : false;
var appendTo = attrs.typeaheadAppendTo ?
originalScope.$eval(attrs.typeaheadAppendTo) : null;
var focusFirst = originalScope.$eval(attrs.typeaheadFocusFirst) !== false;
//If input matches an item of the list exactly, select it automatically
var selectOnExact = attrs.typeaheadSelectOnExact ? originalScope.$eval(attrs.typeaheadSelectOnExact) : false;
//binding to a variable that indicates if dropdown is open
var isOpenSetter = $parse(attrs.typeaheadIsOpen).assign || angular.noop;
var showHint = originalScope.$eval(attrs.typeaheadShowHint) || false;
//INTERNAL VARIABLES
//model setter executed upon match selection
var parsedModel = $parse(attrs.ngModel);
var invokeModelSetter = $parse(attrs.ngModel + '($$$p)');
var $setModelValue = function(scope, newValue) {
if (angular.isFunction(parsedModel(originalScope)) &&
ngModelOptions.getOption('getterSetter')) {
return invokeModelSetter(scope, {$$$p: newValue});
}
return parsedModel.assign(scope, newValue);
};
//expressions used by typeahead
var parserResult = typeaheadParser.parse(attrs.uibTypeahead);
var hasFocus;
//Used to avoid bug in iOS webview where iOS keyboard does not fire
//mousedown & mouseup events
//Issue #3699
var selected;
//create a child scope for the typeahead directive so we are not polluting original scope
//with typeahead-specific data (matches, query etc.)
var scope = originalScope.$new();
var offDestroy = originalScope.$on('$destroy', function() {
scope.$destroy();
});
scope.$on('$destroy', offDestroy);
// WAI-ARIA
var popupId = 'typeahead-' + scope.$id + '-' + Math.floor(Math.random() * 10000);
element.attr({
'aria-autocomplete': 'list',
'aria-expanded': false,
'aria-owns': popupId
});
var inputsContainer, hintInputElem;
//add read-only input to show hint
if (showHint) {
inputsContainer = angular.element('
');
inputsContainer.css('position', 'relative');
element.after(inputsContainer);
hintInputElem = element.clone();
hintInputElem.attr('placeholder', '');
hintInputElem.attr('tabindex', '-1');
hintInputElem.val('');
hintInputElem.css({
'position': 'absolute',
'top': '0px',
'left': '0px',
'border-color': 'transparent',
'box-shadow': 'none',
'opacity': 1,
'background': 'none 0% 0% / auto repeat scroll padding-box border-box rgb(255, 255, 255)',
'color': '#999'
});
element.css({
'position': 'relative',
'vertical-align': 'top',
'background-color': 'transparent'
});
if (hintInputElem.attr('id')) {
hintInputElem.removeAttr('id'); // remove duplicate id if present.
}
inputsContainer.append(hintInputElem);
hintInputElem.after(element);
}
//pop-up element used to display matches
var popUpEl = angular.element('
');
popUpEl.attr({
id: popupId,
matches: 'matches',
active: 'activeIdx',
select: 'select(activeIdx, evt)',
'move-in-progress': 'moveInProgress',
query: 'query',
position: 'position',
'assign-is-open': 'assignIsOpen(isOpen)',
debounce: 'debounceUpdate'
});
//custom item template
if (angular.isDefined(attrs.typeaheadTemplateUrl)) {
popUpEl.attr('template-url', attrs.typeaheadTemplateUrl);
}
if (angular.isDefined(attrs.typeaheadPopupTemplateUrl)) {
popUpEl.attr('popup-template-url', attrs.typeaheadPopupTemplateUrl);
}
var resetHint = function() {
if (showHint) {
hintInputElem.val('');
}
};
var resetMatches = function() {
scope.matches = [];
scope.activeIdx = -1;
element.attr('aria-expanded', false);
resetHint();
};
var getMatchId = function(index) {
return popupId + '-option-' + index;
};
// Indicate that the specified match is the active (pre-selected) item in the list owned by this typeahead.
// This attribute is added or removed automatically when the `activeIdx` changes.
scope.$watch('activeIdx', function(index) {
if (index < 0) {
element.removeAttr('aria-activedescendant');
} else {
element.attr('aria-activedescendant', getMatchId(index));
}
});
var inputIsExactMatch = function(inputValue, index) {
if (scope.matches.length > index && inputValue) {
return inputValue.toUpperCase() === scope.matches[index].label.toUpperCase();
}
return false;
};
var getMatchesAsync = function(inputValue, evt) {
var locals = {$viewValue: inputValue};
isLoadingSetter(originalScope, true);
isNoResultsSetter(originalScope, false);
$q.when(parserResult.source(originalScope, locals)).then(function(matches) {
//it might happen that several async queries were in progress if a user were typing fast
//but we are interested only in responses that correspond to the current view value
var onCurrentRequest = inputValue === modelCtrl.$viewValue;
if (onCurrentRequest && hasFocus) {
if (matches && matches.length > 0) {
scope.activeIdx = focusFirst ? 0 : -1;
isNoResultsSetter(originalScope, false);
scope.matches.length = 0;
//transform labels
for (var i = 0; i < matches.length; i++) {
locals[parserResult.itemName] = matches[i];
scope.matches.push({
id: getMatchId(i),
label: parserResult.viewMapper(scope, locals),
model: matches[i]
});
}
scope.query = inputValue;
//position pop-up with matches - we need to re-calculate its position each time we are opening a window
//with matches as a pop-up might be absolute-positioned and position of an input might have changed on a page
//due to other elements being rendered
recalculatePosition();
element.attr('aria-expanded', true);
//Select the single remaining option if user input matches
if (selectOnExact && scope.matches.length === 1 && inputIsExactMatch(inputValue, 0)) {
if (angular.isNumber(scope.debounceUpdate) || angular.isObject(scope.debounceUpdate)) {
$$debounce(function() {
scope.select(0, evt);
}, angular.isNumber(scope.debounceUpdate) ? scope.debounceUpdate : scope.debounceUpdate['default']);
} else {
scope.select(0, evt);
}
}
if (showHint) {
var firstLabel = scope.matches[0].label;
if (angular.isString(inputValue) &&
inputValue.length > 0 &&
firstLabel.slice(0, inputValue.length).toUpperCase() === inputValue.toUpperCase()) {
hintInputElem.val(inputValue + firstLabel.slice(inputValue.length));
} else {
hintInputElem.val('');
}
}
} else {
resetMatches();
isNoResultsSetter(originalScope, true);
}
}
if (onCurrentRequest) {
isLoadingSetter(originalScope, false);
}
}, function() {
resetMatches();
isLoadingSetter(originalScope, false);
isNoResultsSetter(originalScope, true);
});
};
// bind events only if appendToBody params exist - performance feature
if (appendToBody) {
angular.element($window).on('resize', fireRecalculating);
$document.find('body').on('scroll', fireRecalculating);
}
// Declare the debounced function outside recalculating for
// proper debouncing
var debouncedRecalculate = $$debounce(function() {
// if popup is visible
if (scope.matches.length) {
recalculatePosition();
}
scope.moveInProgress = false;
}, eventDebounceTime);
// Default progress type
scope.moveInProgress = false;
function fireRecalculating() {
if (!scope.moveInProgress) {
scope.moveInProgress = true;
scope.$digest();
}
debouncedRecalculate();
}
// recalculate actual position and set new values to scope
// after digest loop is popup in right position
function recalculatePosition() {
scope.position = appendToBody ? $position.offset(element) : $position.position(element);
scope.position.top += element.prop('offsetHeight');
}
//we need to propagate user's query so we can higlight matches
scope.query = undefined;
//Declare the timeout promise var outside the function scope so that stacked calls can be cancelled later
var timeoutPromise;
var scheduleSearchWithTimeout = function(inputValue) {
timeoutPromise = $timeout(function() {
getMatchesAsync(inputValue);
}, waitTime);
};
var cancelPreviousTimeout = function() {
if (timeoutPromise) {
$timeout.cancel(timeoutPromise);
}
};
resetMatches();
scope.assignIsOpen = function (isOpen) {
isOpenSetter(originalScope, isOpen);
};
scope.select = function(activeIdx, evt) {
//called from within the $digest() cycle
var locals = {};
var model, item;
selected = true;
locals[parserResult.itemName] = item = scope.matches[activeIdx].model;
model = parserResult.modelMapper(originalScope, locals);
$setModelValue(originalScope, model);
modelCtrl.$setValidity('editable', true);
modelCtrl.$setValidity('parse', true);
onSelectCallback(originalScope, {
$item: item,
$model: model,
$label: parserResult.viewMapper(originalScope, locals),
$event: evt
});
resetMatches();
//return focus to the input element if a match was selected via a mouse click event
// use timeout to avoid $rootScope:inprog error
if (scope.$eval(attrs.typeaheadFocusOnSelect) !== false) {
$timeout(function() { element[0].focus(); }, 0, false);
}
};
//bind keyboard events: arrows up(38) / down(40), enter(13) and tab(9), esc(27)
element.on('keydown', function(evt) {
//typeahead is open and an "interesting" key was pressed
if (scope.matches.length === 0 || HOT_KEYS.indexOf(evt.which) === -1) {
return;
}
var shouldSelect = isSelectEvent(originalScope, {$event: evt});
/**
* if there's nothing selected (i.e. focusFirst) and enter or tab is hit
* or
* shift + tab is pressed to bring focus to the previous element
* then clear the results
*/
if (scope.activeIdx === -1 && shouldSelect || evt.which === 9 && !!evt.shiftKey) {
resetMatches();
scope.$digest();
return;
}
evt.preventDefault();
var target;
switch (evt.which) {
case 27: // escape
evt.stopPropagation();
resetMatches();
originalScope.$digest();
break;
case 38: // up arrow
scope.activeIdx = (scope.activeIdx > 0 ? scope.activeIdx : scope.matches.length) - 1;
scope.$digest();
target = popUpEl[0].querySelectorAll('.uib-typeahead-match')[scope.activeIdx];
target.parentNode.scrollTop = target.offsetTop;
break;
case 40: // down arrow
scope.activeIdx = (scope.activeIdx + 1) % scope.matches.length;
scope.$digest();
target = popUpEl[0].querySelectorAll('.uib-typeahead-match')[scope.activeIdx];
target.parentNode.scrollTop = target.offsetTop;
break;
default:
if (shouldSelect) {
scope.$apply(function() {
if (angular.isNumber(scope.debounceUpdate) || angular.isObject(scope.debounceUpdate)) {
$$debounce(function() {
scope.select(scope.activeIdx, evt);
}, angular.isNumber(scope.debounceUpdate) ? scope.debounceUpdate : scope.debounceUpdate['default']);
} else {
scope.select(scope.activeIdx, evt);
}
});
}
}
});
element.on('focus', function (evt) {
hasFocus = true;
if (minLength === 0 && !modelCtrl.$viewValue) {
$timeout(function() {
getMatchesAsync(modelCtrl.$viewValue, evt);
}, 0);
}
});
element.on('blur', function(evt) {
if (isSelectOnBlur && scope.matches.length && scope.activeIdx !== -1 && !selected) {
selected = true;
scope.$apply(function() {
if (angular.isObject(scope.debounceUpdate) && angular.isNumber(scope.debounceUpdate.blur)) {
$$debounce(function() {
scope.select(scope.activeIdx, evt);
}, scope.debounceUpdate.blur);
} else {
scope.select(scope.activeIdx, evt);
}
});
}
if (!isEditable && modelCtrl.$error.editable) {
modelCtrl.$setViewValue();
scope.$apply(function() {
// Reset validity as we are clearing
modelCtrl.$setValidity('editable', true);
modelCtrl.$setValidity('parse', true);
});
element.val('');
}
hasFocus = false;
selected = false;
});
// Keep reference to click handler to unbind it.
var dismissClickHandler = function(evt) {
// Issue #3973
// Firefox treats right click as a click on document
if (element[0] !== evt.target && evt.which !== 3 && scope.matches.length !== 0) {
resetMatches();
if (!$rootScope.$$phase) {
originalScope.$digest();
}
}
};
$document.on('click', dismissClickHandler);
originalScope.$on('$destroy', function() {
$document.off('click', dismissClickHandler);
if (appendToBody || appendTo) {
$popup.remove();
}
if (appendToBody) {
angular.element($window).off('resize', fireRecalculating);
$document.find('body').off('scroll', fireRecalculating);
}
// Prevent jQuery cache memory leak
popUpEl.remove();
if (showHint) {
inputsContainer.remove();
}
});
var $popup = $compile(popUpEl)(scope);
if (appendToBody) {
$document.find('body').append($popup);
} else if (appendTo) {
angular.element(appendTo).eq(0).append($popup);
} else {
element.after($popup);
}
this.init = function(_modelCtrl) {
modelCtrl = _modelCtrl;
ngModelOptions = extractOptions(modelCtrl);
scope.debounceUpdate = $parse(ngModelOptions.getOption('debounce'))(originalScope);
//plug into $parsers pipeline to open a typeahead on view changes initiated from DOM
//$parsers kick-in on all the changes coming from the view as well as manually triggered by $setViewValue
modelCtrl.$parsers.unshift(function(inputValue) {
hasFocus = true;
if (minLength === 0 || inputValue && inputValue.length >= minLength) {
if (waitTime > 0) {
cancelPreviousTimeout();
scheduleSearchWithTimeout(inputValue);
} else {
getMatchesAsync(inputValue);
}
} else {
isLoadingSetter(originalScope, false);
cancelPreviousTimeout();
resetMatches();
}
if (isEditable) {
return inputValue;
}
if (!inputValue) {
// Reset in case user had typed something previously.
modelCtrl.$setValidity('editable', true);
return null;
}
modelCtrl.$setValidity('editable', false);
return undefined;
});
modelCtrl.$formatters.push(function(modelValue) {
var candidateViewValue, emptyViewValue;
var locals = {};
// The validity may be set to false via $parsers (see above) if
// the model is restricted to selected values. If the model
// is set manually it is considered to be valid.
if (!isEditable) {
modelCtrl.$setValidity('editable', true);
}
if (inputFormatter) {
locals.$model = modelValue;
return inputFormatter(originalScope, locals);
}
//it might happen that we don't have enough info to properly render input value
//we need to check for this situation and simply return model value if we can't apply custom formatting
locals[parserResult.itemName] = modelValue;
candidateViewValue = parserResult.viewMapper(originalScope, locals);
locals[parserResult.itemName] = undefined;
emptyViewValue = parserResult.viewMapper(originalScope, locals);
return candidateViewValue !== emptyViewValue ? candidateViewValue : modelValue;
});
};
function extractOptions(ngModelCtrl) {
var ngModelOptions;
if (angular.version.minor < 6) { // in angular < 1.6 $options could be missing
// guarantee a value
ngModelOptions = ngModelCtrl.$options || {};
// mimic 1.6+ api
ngModelOptions.getOption = function (key) {
return ngModelOptions[key];
};
} else { // in angular >=1.6 $options is always present
ngModelOptions = ngModelCtrl.$options;
}
return ngModelOptions;
}
}])
.directive('uibTypeahead', function() {
return {
controller: 'UibTypeaheadController',
require: ['ngModel', 'uibTypeahead'],
link: function(originalScope, element, attrs, ctrls) {
ctrls[1].init(ctrls[0]);
}
};
})
.directive('uibTypeaheadPopup', ['$$debounce', function($$debounce) {
return {
scope: {
matches: '=',
query: '=',
active: '=',
position: '&',
moveInProgress: '=',
select: '&',
assignIsOpen: '&',
debounce: '&'
},
replace: true,
templateUrl: function(element, attrs) {
return attrs.popupTemplateUrl || 'uib/template/typeahead/typeahead-popup.html';
},
link: function(scope, element, attrs) {
scope.templateUrl = attrs.templateUrl;
scope.isOpen = function() {
var isDropdownOpen = scope.matches.length > 0;
scope.assignIsOpen({ isOpen: isDropdownOpen });
return isDropdownOpen;
};
scope.isActive = function(matchIdx) {
return scope.active === matchIdx;
};
scope.selectActive = function(matchIdx) {
scope.active = matchIdx;
};
scope.selectMatch = function(activeIdx, evt) {
var debounce = scope.debounce();
if (angular.isNumber(debounce) || angular.isObject(debounce)) {
$$debounce(function() {
scope.select({activeIdx: activeIdx, evt: evt});
}, angular.isNumber(debounce) ? debounce : debounce['default']);
} else {
scope.select({activeIdx: activeIdx, evt: evt});
}
};
}
};
}])
.directive('uibTypeaheadMatch', ['$templateRequest', '$compile', '$parse', function($templateRequest, $compile, $parse) {
return {
scope: {
index: '=',
match: '=',
query: '='
},
link: function(scope, element, attrs) {
var tplUrl = $parse(attrs.templateUrl)(scope.$parent) || 'uib/template/typeahead/typeahead-match.html';
$templateRequest(tplUrl).then(function(tplContent) {
var tplEl = angular.element(tplContent.trim());
element.replaceWith(tplEl);
$compile(tplEl)(scope);
});
}
};
}])
.filter('uibTypeaheadHighlight', ['$sce', '$injector', '$log', function($sce, $injector, $log) {
var isSanitizePresent;
isSanitizePresent = $injector.has('$sanitize');
function escapeRegexp(queryToEscape) {
// Regex: capture the whole query string and replace it with the string that will be used to match
// the results, for example if the capture is "a" the result will be \a
return queryToEscape.replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1');
}
function containsHtml(matchItem) {
return /<.*>/g.test(matchItem);
}
return function(matchItem, query) {
if (!isSanitizePresent && containsHtml(matchItem)) {
$log.warn('Unsafe use of typeahead please use ngSanitize'); // Warn the user about the danger
}
matchItem = query ? ('' + matchItem).replace(new RegExp(escapeRegexp(query), 'gi'), '
$&') : matchItem; // Replaces the capture string with a the same string inside of a "strong" tag
if (!isSanitizePresent) {
matchItem = $sce.trustAsHtml(matchItem); // If $sanitize is not present we pack the string in a $sce object for the ng-bind-html directive
}
return matchItem;
};
}]);
================================================
FILE: template/accordion/accordion-group.html
================================================
================================================
FILE: template/accordion/accordion.html
================================================
================================================
FILE: template/alert/alert.html
================================================
================================================
FILE: template/carousel/carousel.html
================================================
previous
next
-
slide {{ $index + 1 }} of {{ slides.length }}, currently active
================================================
FILE: template/carousel/slide.html
================================================
================================================
FILE: template/datepicker/datepicker.html
================================================
================================================
FILE: template/datepicker/day.html
================================================
|
|
|
|
{{::label.abbr}} |
| {{ weekNumbers[$index] }} |
|
================================================
FILE: template/datepicker/month.html
================================================
|
|
|
|
|
================================================
FILE: template/datepicker/year.html
================================================
|
|
|
|
|
================================================
FILE: template/datepickerPopup/popup.html
================================================
================================================
FILE: template/modal/window.html
================================================
================================================
FILE: template/pager/pager.html
================================================
{{::getText('previous')}}
{{::getText('next')}}
================================================
FILE: template/pagination/pagination.html
================================================
================================================
FILE: template/popover/popover-html.html
================================================
================================================
FILE: template/popover/popover-template.html
================================================
================================================
FILE: template/popover/popover.html
================================================
================================================
FILE: template/progressbar/bar.html
================================================
================================================
FILE: template/progressbar/progress.html
================================================
================================================
FILE: template/progressbar/progressbar.html
================================================
================================================
FILE: template/rating/rating.html
================================================
({{ $index < value ? '*' : ' ' }})
================================================
FILE: template/tabs/tab.html
================================================
{{heading}}
================================================
FILE: template/tabs/tabset.html
================================================
================================================
FILE: template/timepicker/timepicker.html
================================================
================================================
FILE: template/tooltip/tooltip-html-popup.html
================================================
================================================
FILE: template/tooltip/tooltip-popup.html
================================================
================================================
FILE: template/tooltip/tooltip-template-popup.html
================================================
================================================
FILE: template/typeahead/typeahead-match.html
================================================
================================================
FILE: template/typeahead/typeahead-popup.html
================================================