Merged source

This commit is contained in:
Martin Schröder 2015-04-21 08:55:46 +02:00 committed by Martin Schröder
parent 5f93c92c2c
commit 6897e61b2c
890 changed files with 247359 additions and 5900 deletions

View file

@ -0,0 +1,5 @@
cmake_minimum_required(VERSION 2.6)
PROJECT(luciexpress C)
ADD_SUBDIRECTORY(src)

19
luciexpress/Gruntfile.js Normal file
View file

@ -0,0 +1,19 @@
module.exports = function(grunt){
grunt.loadNpmTasks('grunt-angular-gettext');
grunt.initConfig({
nggettext_extract: {
pot: {
files: {
'po/template.pot': ['**/*.html', 'js/*.js']
}
}
},
nggettext_compile: {
all: {
files: {
'htdocs/lib/translations.js': ['po/*.po']
}
}
}
});
}

View file

@ -3,34 +3,35 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luciexpress
PKG_VERSION:=master
PKG_MAINTAINER:=Martin K. Schröder <martin.schroder@inteno.se>
PKG_SOURCE_URL:=https://github.com/mkschreder/luci-express.git
#PKG_SOURCE_URL:=https://github.com/mkschreder/luci-express.git
#PKG_SOURCE_URL:=/home/martin/luci-express
PKG_SOURCE_PROTO:=git
PKG_SOURCE_VERSION:=HEAD
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION)
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION)-$(PKG_SOURCE_VERSION).tar.gz
#PKG_SOURCE_PROTO:=git
#PKG_SOURCE_VERSION:=HEAD
#PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION)
#PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION)-$(PKG_SOURCE_VERSION).tar.gz
PKG_RELEASE:=3
include $(INCLUDE_DIR)/package.mk
include $(INCLUDE_DIR)/cmake.mk
PKG_FIXUP:=autoreconf
PKG_INSTALL=1
define Build/Configure
#define Build/Configure
#(cd $(PKG_BUILD_DIR) && $(BASH) -x ./bootstrap)
$(CP) ./share ./htdocs $(PKG_BUILD_DIR)/
$(call Build/Configure/Default)
# $(CP) ./share ./htdocs $(PKG_BUILD_DIR)/
# $(call Build/Configure/Default)
#endef
define Build/Prepare
$(INSTALL_DIR) $(PKG_BUILD_DIR)
$(CP) ./src/* $(PKG_BUILD_DIR)/
endef
define Package/luciexpress
SECTION:=luciexpress
CATEGORY:=LuCIexpress
TITLE:=LuCIexpress UI
CATEGORY:=LuCiexpress
TITLE:=LuCiExpress UI
DEPENDS:=+rpcd +rpcd-mod-iwinfo +uhttpd +uhttpd-mod-ubus +libubox +libubus
endef
@ -40,14 +41,14 @@ endef
define Package/luciexpress/install
$(INSTALL_DIR) $(1)/www
$(CP) $(PKG_BUILD_DIR)/htdocs/* $(1)/www/
$(CP) ./htdocs/* $(1)/www/
$(INSTALL_DIR) $(1)/usr/share/rpcd
$(CP) $(PKG_BUILD_DIR)/share/* $(1)/usr/share/rpcd/
$(CP) ./share/* $(1)/usr/share/rpcd/
$(INSTALL_DIR) $(1)/usr/lib/rpcd
$(INSTALL_BIN) $(PKG_BUILD_DIR)/src/rpcd/luciexpress.so $(1)/usr/lib/rpcd/
$(INSTALL_BIN) $(PKG_BUILD_DIR)/src/rpcd/bwmon.so $(1)/usr/lib/rpcd/
$(INSTALL_BIN) $(PKG_BUILD_DIR)/rpcd/luciexpress.so $(1)/usr/lib/rpcd/
$(INSTALL_BIN) $(PKG_BUILD_DIR)/rpcd/bwmon.so $(1)/usr/lib/rpcd/
$(INSTALL_DIR) $(1)/usr/libexec $(1)/www/cgi-bin
$(INSTALL_BIN) $(PKG_BUILD_DIR)/src/io/luciexpress-io $(1)/usr/libexec/
$(INSTALL_BIN) $(PKG_BUILD_DIR)/io/luciexpress-io $(1)/usr/libexec/
$(LN) /usr/libexec/luciexpress-io $(1)/www/cgi-bin/luci-upload
$(LN) /usr/libexec/luciexpress-io $(1)/www/cgi-bin/luci-backup
endef

8
luciexpress/README.md Normal file
View file

@ -0,0 +1,8 @@
LuCi Express
------------
This is an effort to simplify luci web gui for OpenWRT using modern frameworks and coding standards like angular.js.
LuCi express is a client-side JavaScript/HTML5 application that communicates with your OpenWRT router over ubus calls (JSONRPC2.0).
To run the gui, simply place htdocs directory into the web root of your router and navigate to the htdocs/index.html page.

5
luciexpress/bootstrap.sh Executable file
View file

@ -0,0 +1,5 @@
#!/bin/bash
npm install
sudo npm install -g grunt-cli
sudo npm install -g bower

4
luciexpress/call_rpc.sh Normal file
View file

@ -0,0 +1,4 @@
curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "method":"call","params":["","session","access",{"scope": "ubus", "object": "router", "function": "info"}], "id":"1"}' http://192.168.1.1/ubus
curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "method":"call","params":["00000000000000000000000000000000","router","dslstats",{}], "id":"1"}' http://192.168.1.1/ubus

4
luciexpress/compile_strings.sh Executable file
View file

@ -0,0 +1,4 @@
#!/bin/bash
grunt nggettext_extract
grunt nggettext_compile

View file

@ -0,0 +1,21 @@
{
"name": "luci",
"version": "1.0.0",
"main": "css/app.css",
"ignore": [
".jshintrc",
"**/*.txt"
],
"dependencies": {
"angular": "*",
"angular-gettext": "*",
"angular-ui": "*",
"angular-ui-bootstrap": "*",
"angular-ui-router": "*",
"bootstrap": "*",
"jquery": "*"
},
"devDependencies": {
}
}

View file

@ -0,0 +1,31 @@
{
"name": "angular-gettext",
"version": "2.0.5",
"main": "dist/angular-gettext.js",
"ignore": [
"**/.*",
"src",
"node_modules",
"bower_components",
"test",
"genplurals.py",
"Gruntfile.js"
],
"dependencies": {
"angular": ">=1.2.0"
},
"devDependencies": {
"jquery": ">=1.8.0",
"angular-mocks": ">=1.2.0"
},
"homepage": "https://github.com/rubenv/angular-gettext",
"_release": "2.0.5",
"_resolution": {
"type": "version",
"tag": "v2.0.5",
"commit": "314be85efd94c0a1670a051c8de17aa26c6ffab1"
},
"_source": "git://github.com/rubenv/angular-gettext.git",
"_target": "*",
"_originalSource": "angular-gettext"
}

View file

@ -0,0 +1,19 @@
Copyright (C) 2013-2015 by Ruben Vermeersch <ruben@rocketeer.be>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View file

@ -0,0 +1,31 @@
# angular-gettext - gettext utilities for angular.js
> Translate your Angular.JS applications with gettext.
[![Build Status](https://travis-ci.org/rubenv/angular-gettext.png?branch=master)](https://travis-ci.org/rubenv/angular-gettext)
Check the website for usage instructions: [http://angular-gettext.rocketeer.be/](http://angular-gettext.rocketeer.be/).
## License
(The MIT License)
Copyright (C) 2013-2015 by Ruben Vermeersch <ruben@rocketeer.be>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View file

@ -0,0 +1,21 @@
{
"name": "angular-gettext",
"version": "2.0.5",
"main": "dist/angular-gettext.js",
"ignore": [
"**/.*",
"src",
"node_modules",
"bower_components",
"test",
"genplurals.py",
"Gruntfile.js"
],
"dependencies": {
"angular": ">=1.2.0"
},
"devDependencies": {
"jquery": ">=1.8.0",
"angular-mocks": ">=1.2.0"
}
}

View file

@ -0,0 +1,335 @@
angular.module('gettext', []);
angular.module('gettext').constant('gettext', function (str) {
/*
* Does nothing, simply returns the input string.
*
* This function serves as a marker for `grunt-angular-gettext` to know that
* this string should be extracted for translations.
*/
return str;
});
angular.module('gettext').factory('gettextCatalog', ["gettextPlurals", "$http", "$cacheFactory", "$interpolate", "$rootScope", function (gettextPlurals, $http, $cacheFactory, $interpolate, $rootScope) {
var catalog;
var noContext = '$$noContext';
// IE8 returns UPPER CASE tags, even though the source is lower case.
// This can causes the (key) string in the DOM to have a different case to
// the string in the `po` files.
// IE9, IE10 and IE11 reorders the attributes of tags.
var test = '<span id="test" title="test" class="tested">test</span>';
var isHTMLModified = (angular.element('<span>' + test + '</span>').html() !== test);
var prefixDebug = function (string) {
if (catalog.debug && catalog.currentLanguage !== catalog.baseLanguage) {
return catalog.debugPrefix + string;
} else {
return string;
}
};
var addTranslatedMarkers = function (string) {
if (catalog.showTranslatedMarkers) {
return catalog.translatedMarkerPrefix + string + catalog.translatedMarkerSuffix;
} else {
return string;
}
};
function broadcastUpdated() {
$rootScope.$broadcast('gettextLanguageChanged');
}
catalog = {
debug: false,
debugPrefix: '[MISSING]: ',
showTranslatedMarkers: false,
translatedMarkerPrefix: '[',
translatedMarkerSuffix: ']',
strings: {},
baseLanguage: 'en',
currentLanguage: 'en',
cache: $cacheFactory('strings'),
setCurrentLanguage: function (lang) {
this.currentLanguage = lang;
broadcastUpdated();
},
getCurrentLanguage: function () {
return this.currentLanguage;
},
setStrings: function (language, strings) {
if (!this.strings[language]) {
this.strings[language] = {};
}
for (var key in strings) {
var val = strings[key];
if (isHTMLModified) {
// Use the DOM engine to render any HTML in the key (#131).
key = angular.element('<span>' + key + '</span>').html();
}
if (angular.isString(val) || angular.isArray(val)) {
// No context, wrap it in $$noContext.
var obj = {};
obj[noContext] = val;
val = obj;
}
// Expand single strings for each context.
for (var context in val) {
var str = val[context];
val[context] = angular.isArray(str) ? str : [str];
}
this.strings[language][key] = val;
}
broadcastUpdated();
},
getStringForm: function (string, n, context) {
var stringTable = this.strings[this.currentLanguage] || {};
var contexts = stringTable[string] || {};
var plurals = contexts[context || noContext] || [];
return plurals[n];
},
getString: function (string, scope, context) {
string = this.getStringForm(string, 0, context) || prefixDebug(string);
string = scope ? $interpolate(string)(scope) : string;
return addTranslatedMarkers(string);
},
getPlural: function (n, string, stringPlural, scope, context) {
var form = gettextPlurals(this.currentLanguage, n);
string = this.getStringForm(string, form, context) || prefixDebug(n === 1 ? string : stringPlural);
if (scope) {
scope.$count = n;
string = $interpolate(string)(scope);
}
return addTranslatedMarkers(string);
},
loadRemote: function (url) {
return $http({
method: 'GET',
url: url,
cache: catalog.cache
}).success(function (data) {
for (var lang in data) {
catalog.setStrings(lang, data[lang]);
}
});
}
};
return catalog;
}]);
angular.module('gettext').directive('translate', ["gettextCatalog", "$parse", "$animate", "$compile", "$window", function (gettextCatalog, $parse, $animate, $compile, $window) {
// Trim polyfill for old browsers (instead of jQuery)
// Based on AngularJS-v1.2.2 (angular.js#620)
var trim = (function () {
if (!String.prototype.trim) {
return function (value) {
return (typeof value === 'string') ? value.replace(/^\s*/, '').replace(/\s*$/, '') : value;
};
}
return function (value) {
return (typeof value === 'string') ? value.trim() : value;
};
})();
function assert(condition, missing, found) {
if (!condition) {
throw new Error('You should add a ' + missing + ' attribute whenever you add a ' + found + ' attribute.');
}
}
var msie = parseInt((/msie (\d+)/.exec(angular.lowercase($window.navigator.userAgent)) || [])[1], 10);
return {
restrict: 'AE',
terminal: true,
compile: function compile(element, attrs) {
// Validate attributes
assert(!attrs.translatePlural || attrs.translateN, 'translate-n', 'translate-plural');
assert(!attrs.translateN || attrs.translatePlural, 'translate-plural', 'translate-n');
var msgid = trim(element.html());
var translatePlural = attrs.translatePlural;
var translateContext = attrs.translateContext;
if (msie <= 8) {
// Workaround fix relating to angular adding a comment node to
// anchors. angular/angular.js/#1949 / angular/angular.js/#2013
if (msgid.slice(-13) === '<!--IE fix-->') {
msgid = msgid.slice(0, -13);
}
}
return {
post: function (scope, element, attrs) {
var countFn = $parse(attrs.translateN);
var pluralScope = null;
function update() {
// Fetch correct translated string.
var translated;
if (translatePlural) {
scope = pluralScope || (pluralScope = scope.$new());
scope.$count = countFn(scope);
translated = gettextCatalog.getPlural(scope.$count, msgid, translatePlural, null, translateContext);
} else {
translated = gettextCatalog.getString(msgid, null, translateContext);
}
// Swap in the translation
var newWrapper = angular.element('<span>' + translated + '</span>');
$compile(newWrapper.contents())(scope);
var oldContents = element.contents();
var newContents = newWrapper.contents();
$animate.enter(newContents, element);
$animate.leave(oldContents);
}
if (attrs.translateN) {
scope.$watch(attrs.translateN, update);
}
scope.$on('gettextLanguageChanged', update);
update();
}
};
}
};
}]);
angular.module('gettext').filter('translate', ["gettextCatalog", function (gettextCatalog) {
function filter(input, context) {
return gettextCatalog.getString(input, null, context);
}
filter.$stateful = true;
return filter;
}]);
// Do not edit this file, it is autogenerated using genplurals.py!
angular.module("gettext").factory("gettextPlurals", function () {
return function (langCode, n) {
switch (langCode) {
case "ay": // Aymará
case "bo": // Tibetan
case "cgg": // Chiga
case "dz": // Dzongkha
case "fa": // Persian
case "id": // Indonesian
case "ja": // Japanese
case "jbo": // Lojban
case "ka": // Georgian
case "kk": // Kazakh
case "km": // Khmer
case "ko": // Korean
case "ky": // Kyrgyz
case "lo": // Lao
case "ms": // Malay
case "my": // Burmese
case "sah": // Yakut
case "su": // Sundanese
case "th": // Thai
case "tt": // Tatar
case "ug": // Uyghur
case "vi": // Vietnamese
case "wo": // Wolof
case "zh": // Chinese
// 1 form
return 0;
case "is": // Icelandic
// 2 forms
return (n%10!=1 || n%100==11) ? 1 : 0;
case "jv": // Javanese
// 2 forms
return n!=0 ? 1 : 0;
case "mk": // Macedonian
// 2 forms
return n==1 || n%10==1 ? 0 : 1;
case "ach": // Acholi
case "ak": // Akan
case "am": // Amharic
case "arn": // Mapudungun
case "br": // Breton
case "fil": // Filipino
case "fr": // French
case "gun": // Gun
case "ln": // Lingala
case "mfe": // Mauritian Creole
case "mg": // Malagasy
case "mi": // Maori
case "oc": // Occitan
case "pt_BR": // Brazilian Portuguese
case "tg": // Tajik
case "ti": // Tigrinya
case "tr": // Turkish
case "uz": // Uzbek
case "wa": // Walloon
case "zh": // Chinese
// 2 forms
return n>1 ? 1 : 0;
case "lv": // Latvian
// 3 forms
return (n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2);
case "lt": // Lithuanian
// 3 forms
return (n%10==1 && n%100!=11 ? 0 : n%10>=2 && (n%100<10 || n%100>=20) ? 1 : 2);
case "be": // Belarusian
case "bs": // Bosnian
case "hr": // Croatian
case "ru": // Russian
case "sr": // Serbian
case "uk": // Ukrainian
// 3 forms
return (n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);
case "mnk": // Mandinka
// 3 forms
return (n==0 ? 0 : n==1 ? 1 : 2);
case "ro": // Romanian
// 3 forms
return (n==1 ? 0 : (n==0 || (n%100 > 0 && n%100 < 20)) ? 1 : 2);
case "pl": // Polish
// 3 forms
return (n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);
case "cs": // Czech
case "sk": // Slovak
// 3 forms
return (n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;
case "sl": // Slovenian
// 4 forms
return (n%100==1 ? 1 : n%100==2 ? 2 : n%100==3 || n%100==4 ? 3 : 0);
case "mt": // Maltese
// 4 forms
return (n==1 ? 0 : n==0 || ( n%100>1 && n%100<11) ? 1 : (n%100>10 && n%100<20 ) ? 2 : 3);
case "gd": // Scottish Gaelic
// 4 forms
return (n==1 || n==11) ? 0 : (n==2 || n==12) ? 1 : (n > 2 && n < 20) ? 2 : 3;
case "cy": // Welsh
// 4 forms
return (n==1) ? 0 : (n==2) ? 1 : (n != 8 && n != 11) ? 2 : 3;
case "kw": // Cornish
// 4 forms
return (n==1) ? 0 : (n==2) ? 1 : (n == 3) ? 2 : 3;
case "ga": // Irish
// 5 forms
return n==1 ? 0 : n==2 ? 1 : n<7 ? 2 : n<11 ? 3 : 4;
case "ar": // Arabic
// 6 forms
return (n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 ? 4 : 5);
default: // Everything else
return n != 1 ? 1 : 0;
}
}
});

View file

@ -0,0 +1 @@
angular.module("gettext",[]),angular.module("gettext").constant("gettext",function(a){return a}),angular.module("gettext").factory("gettextCatalog",["gettextPlurals","$http","$cacheFactory","$interpolate","$rootScope",function(a,b,c,d,e){function f(){e.$broadcast("gettextLanguageChanged")}var g,h="$$noContext",i='<span id="test" title="test" class="tested">test</span>',j=angular.element("<span>"+i+"</span>").html()!==i,k=function(a){return g.debug&&g.currentLanguage!==g.baseLanguage?g.debugPrefix+a:a},l=function(a){return g.showTranslatedMarkers?g.translatedMarkerPrefix+a+g.translatedMarkerSuffix:a};return g={debug:!1,debugPrefix:"[MISSING]: ",showTranslatedMarkers:!1,translatedMarkerPrefix:"[",translatedMarkerSuffix:"]",strings:{},baseLanguage:"en",currentLanguage:"en",cache:c("strings"),setCurrentLanguage:function(a){this.currentLanguage=a,f()},getCurrentLanguage:function(){return this.currentLanguage},setStrings:function(a,b){this.strings[a]||(this.strings[a]={});for(var c in b){var d=b[c];if(j&&(c=angular.element("<span>"+c+"</span>").html()),angular.isString(d)||angular.isArray(d)){var e={};e[h]=d,d=e}for(var g in d){var i=d[g];d[g]=angular.isArray(i)?i:[i]}this.strings[a][c]=d}f()},getStringForm:function(a,b,c){var d=this.strings[this.currentLanguage]||{},e=d[a]||{},f=e[c||h]||[];return f[b]},getString:function(a,b,c){return a=this.getStringForm(a,0,c)||k(a),a=b?d(a)(b):a,l(a)},getPlural:function(b,c,e,f,g){var h=a(this.currentLanguage,b);return c=this.getStringForm(c,h,g)||k(1===b?c:e),f&&(f.$count=b,c=d(c)(f)),l(c)},loadRemote:function(a){return b({method:"GET",url:a,cache:g.cache}).success(function(a){for(var b in a)g.setStrings(b,a[b])})}}}]),angular.module("gettext").directive("translate",["gettextCatalog","$parse","$animate","$compile","$window",function(a,b,c,d,e){function f(a,b,c){if(!a)throw new Error("You should add a "+b+" attribute whenever you add a "+c+" attribute.")}var g=function(){return String.prototype.trim?function(a){return"string"==typeof a?a.trim():a}:function(a){return"string"==typeof a?a.replace(/^\s*/,"").replace(/\s*$/,""):a}}(),h=parseInt((/msie (\d+)/.exec(angular.lowercase(e.navigator.userAgent))||[])[1],10);return{restrict:"AE",terminal:!0,compile:function(e,i){f(!i.translatePlural||i.translateN,"translate-n","translate-plural"),f(!i.translateN||i.translatePlural,"translate-plural","translate-n");var j=g(e.html()),k=i.translatePlural,l=i.translateContext;return 8>=h&&"<!--IE fix-->"===j.slice(-13)&&(j=j.slice(0,-13)),{post:function(e,f,g){function h(){var b;k?(e=m||(m=e.$new()),e.$count=i(e),b=a.getPlural(e.$count,j,k,null,l)):b=a.getString(j,null,l);var g=angular.element("<span>"+b+"</span>");d(g.contents())(e);var h=f.contents(),n=g.contents();c.enter(n,f),c.leave(h)}var i=b(g.translateN),m=null;g.translateN&&e.$watch(g.translateN,h),e.$on("gettextLanguageChanged",h),h()}}}}}]),angular.module("gettext").filter("translate",["gettextCatalog",function(a){function b(b,c){return a.getString(b,null,c)}return b.$stateful=!0,b}]),angular.module("gettext").factory("gettextPlurals",function(){return function(a,b){switch(a){case"ay":case"bo":case"cgg":case"dz":case"fa":case"id":case"ja":case"jbo":case"ka":case"kk":case"km":case"ko":case"ky":case"lo":case"ms":case"my":case"sah":case"su":case"th":case"tt":case"ug":case"vi":case"wo":case"zh":return 0;case"is":return b%10!=1||b%100==11?1:0;case"jv":return 0!=b?1:0;case"mk":return 1==b||b%10==1?0:1;case"ach":case"ak":case"am":case"arn":case"br":case"fil":case"fr":case"gun":case"ln":case"mfe":case"mg":case"mi":case"oc":case"pt_BR":case"tg":case"ti":case"tr":case"uz":case"wa":case"zh":return b>1?1:0;case"lv":return b%10==1&&b%100!=11?0:0!=b?1:2;case"lt":return b%10==1&&b%100!=11?0:b%10>=2&&(10>b%100||b%100>=20)?1:2;case"be":case"bs":case"hr":case"ru":case"sr":case"uk":return b%10==1&&b%100!=11?0:b%10>=2&&4>=b%10&&(10>b%100||b%100>=20)?1:2;case"mnk":return 0==b?0:1==b?1:2;case"ro":return 1==b?0:0==b||b%100>0&&20>b%100?1:2;case"pl":return 1==b?0:b%10>=2&&4>=b%10&&(10>b%100||b%100>=20)?1:2;case"cs":case"sk":return 1==b?0:b>=2&&4>=b?1:2;case"sl":return b%100==1?1:b%100==2?2:b%100==3||b%100==4?3:0;case"mt":return 1==b?0:0==b||b%100>1&&11>b%100?1:b%100>10&&20>b%100?2:3;case"gd":return 1==b||11==b?0:2==b||12==b?1:b>2&&20>b?2:3;case"cy":return 1==b?0:2==b?1:8!=b&&11!=b?2:3;case"kw":return 1==b?0:2==b?1:3==b?2:3;case"ga":return 1==b?0:2==b?1:7>b?2:11>b?3:4;case"ar":return 0==b?0:1==b?1:2==b?2:b%100>=3&&10>=b%100?3:b%100>=11?4:5;default:return 1!=b?1:0}}});

View file

@ -0,0 +1,48 @@
{
"name": "angular-gettext",
"version": "2.0.5",
"description": "Gettext support for Angular.js",
"main": "dist/angular-gettext.js",
"directories": {
"test": "test"
},
"scripts": {
"test": "grunt ci",
"prepublish": "grunt build"
},
"keywords": [
"angular",
"gettext"
],
"author": {
"name": "Ruben Vermeersch",
"email": "ruben@rocketeer.be",
"url": "http://rocketeer.be/"
},
"homepage": "http://angular-gettext.rocketeer.be/",
"license": "MIT",
"devDependencies": {
"grunt": "~0.4.1",
"grunt-bump": "0.0.13",
"grunt-contrib-clean": "~0.5.0",
"grunt-contrib-concat": "~0.3.0",
"grunt-contrib-connect": "~0.7.1",
"grunt-contrib-jshint": "~0.10.0",
"grunt-contrib-uglify": "~0.4.0",
"grunt-contrib-watch": "~0.5.1",
"grunt-jscs": "^0.6.2",
"grunt-karma": "~0.8.3",
"grunt-ng-annotate": "^0.3.2",
"karma": "~0.12.16",
"karma-chai": "~0.1.0",
"karma-firefox-launcher": "~0.1.0",
"karma-junit-reporter": "~0.2.2",
"karma-mocha": "~0.1.0",
"karma-ng-scenario": "~0.1.0",
"karma-phantomjs-launcher": "^0.1.4"
},
"repository": {
"type": "git",
"url": "git://github.com/rubenv/angular-gettext.git"
}
}

View file

@ -0,0 +1,14 @@
{
"name": "angular-ui-bootstrap",
"homepage": "https://github.com/angular-ui/bootstrap",
"version": "0.12.1",
"_release": "0.12.1",
"_resolution": {
"type": "version",
"tag": "0.12.1",
"commit": "b7e68347adf2404657c8edc1e3f7da5baf6f6082"
},
"_source": "git://github.com/angular-ui/bootstrap.git",
"_target": "*",
"_originalSource": "angular-ui-bootstrap"
}

View file

@ -0,0 +1,18 @@
# This file is for unifying the coding style for different editors and IDEs
# editorconfig.org
root = true
[*]
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
# Tabs in JS unless otherwise specified
[**.js]
indent_style = space
indent_size = 2
[*.md]
trim_trailing_whitespace = false

View file

@ -0,0 +1,6 @@
*.html eol=lf
*.css eol=lf
*.js eol=lf
*.md eol=lf
*.json eol=lf
*.yml eol=lf

View file

@ -0,0 +1,23 @@
lib-cov
*.seed
*.log
*.csv
*.dat
*.out
*.pid
*.gz
*.swp
*.swo
.DS_Store
pids
logs
results
dist
# test coverage files
coverage/
node_modules
npm-debug.log
template/**/*.js

View file

@ -0,0 +1,30 @@
{
"curly": true,
"immed": true,
"newcap": true,
"noarg": true,
"sub": true,
"boss": true,
"eqnull": true,
"quotmark": "single",
"trailing": true,
"undef": true,
"browser": true,
"jquery": true,
"globals": {
"angular": false,
// For Jasmine
"after" : false,
"afterEach" : false,
"before" : false,
"beforeEach" : false,
"describe" : false,
"expect" : false,
"jasmine" : false,
"module" : false,
"spyOn" : false,
"inject" : false,
"it" : false
}
}

View file

@ -0,0 +1,11 @@
language: node_js
node_js:
- "0.10"
before_script:
- export DISPLAY=:99.0
- sh -e /etc/init.d/xvfb start
- npm install --quiet -g grunt-cli karma
- npm install
script: grunt

View file

@ -0,0 +1,824 @@
# 0.12.1 (2015-02-20)
## Bug Fixes
- **tooltip:**
- incorrect position when text wraps ([5726e3ef](http://github.com/angular-ui/bootstrap/commit/5726e3ef))
<a name="0.12.0"></a>
# 0.12.0 (2014-11-16)
## Bug Fixes
* **accordion:** make header links keyboard accessible ([992b2329](angular-ui/bootstrap/commit/992b23297cd100ab4e236fba469e3a70566a4163), closes [#2869](angular-ui/bootstrap/issues/2869))
* **build:** make custom builds on demo site work ([390f2bf6](angular-ui/bootstrap/commit/390f2bf6b0846ee640e86ad87bbae8c449e53026), closes [#2960](angular-ui/bootstrap/issues/2960), [#2847](angular-ui/bootstrap/issues/2847), [#2625](angular-ui/bootstrap/issues/2625), [#2489](angular-ui/bootstrap/issues/2489), [#2357](angular-ui/bootstrap/issues/2357), [#2176](angular-ui/bootstrap/issues/2176), [#2892](angular-ui/bootstrap/issues/2892))
* **carousel:** replaced $timeout with $interval when it was wrong ([392c0ad1](angular-ui/bootstrap/commit/392c0ad13ca9b65be5e77ac0c68de24ead8ea2ce), closes [#1308](angular-ui/bootstrap/issues/1308), [#2454](angular-ui/bootstrap/issues/2454), [#2776](angular-ui/bootstrap/issues/2776))
* **datepicker:** correct button alignment when using bootstrap v3.2.0 ([460fbec7](angular-ui/bootstrap/commit/460fbec776c6d08d0e7db40aedd29d10ac48d7e9), closes [#2728](angular-ui/bootstrap/issues/2728))
* **demo:** initial load of fragment URLs ([eab6daf6](angular-ui/bootstrap/commit/eab6daf64b3c963d8e285e254c75af5f97c24ec1), closes [#2762](angular-ui/bootstrap/issues/2762))
* **dropdown:**
* compatibility with `$location` url rewriting ([ef095170](angular-ui/bootstrap/commit/ef09517061b0b4c0c9e9f85086635af33207ec54), closes [#2343](angular-ui/bootstrap/issues/2343))
* remove `C` restrictions to avoid conflicts ([192768e1](angular-ui/bootstrap/commit/192768e109b5c4a50c7dcd320e09d05fedd4298a), closes [#2156](angular-ui/bootstrap/issues/2156), [#2170](angular-ui/bootstrap/issues/2170))
* **tabs:**
* make tab links keyboard accessible ([5df524b7](angular-ui/bootstrap/commit/5df524b77114bccdc9a49540e1eb52a564ee5dfd), closes [#2226](angular-ui/bootstrap/issues/2226), [#2290](angular-ui/bootstrap/issues/2290), [#2870](angular-ui/bootstrap/issues/2870), [#2304](angular-ui/bootstrap/issues/2304))
* don't select tabs on destroy ([9939867a](angular-ui/bootstrap/commit/9939867aba0b7b763588b18829b557c052ea69ba), closes [#2155](angular-ui/bootstrap/issues/2155), [#2596](angular-ui/bootstrap/issues/2596))
* **tests:** usage of undefined variables ([34273ff0](angular-ui/bootstrap/commit/34273ff0107ecfa28438a7389d94ca619b8704e5))
* **tooltip:**
* remove extra digest causing incompatibility ([32c4704b](angular-ui/bootstrap/commit/32c4704b748cecf2de4c651f2e5157c1ef6c88b1), closes [#2951](angular-ui/bootstrap/issues/2951), [#2959](angular-ui/bootstrap/issues/2959))
* show correct tooltip on `ng-repeat` ([b4832c4b](angular-ui/bootstrap/commit/b4832c4b551af7e580ed65d9e5aaee1ef9e6c53e), closes [#2935](angular-ui/bootstrap/issues/2935))
* memory leak on show/hide ([faf38d20](angular-ui/bootstrap/commit/faf38d20a49176f2016f7f7d4fa49a5c438a986e), closes [#2709](angular-ui/bootstrap/issues/2709), [#2919](angular-ui/bootstrap/issues/2919))
* remove child scope requirement ([8204c808](angular-ui/bootstrap/commit/8204c8088139165ac9b2ad3977a2c20570e434cb), closes [#1269](angular-ui/bootstrap/issues/1269), [#2320](angular-ui/bootstrap/issues/2320), [#2203](angular-ui/bootstrap/issues/2203))
* evaluate appendToBody on init ([e10d561f](angular-ui/bootstrap/commit/e10d561f92c2927be0ec429761fa229520fb9a51), closes [#2921](angular-ui/bootstrap/issues/2921))
* don't use an empty transclusion fn ([689c4d01](angular-ui/bootstrap/commit/689c4d017d303b6d758164ee12837a172bb01139), closes [#2825](angular-ui/bootstrap/issues/2825))
* **typeahead:** don't leak DOM nodes ([1f6c3c92](angular-ui/bootstrap/commit/1f6c3c92af0e343c7e34b85ea6d270ac79bf6755))
## Features
* **alert:** allow alerts to be closed from a controller ([ca6fad67](angular-ui/bootstrap/commit/ca6fad675bf2aa793896bf3e086330667a5d9051), closes [#2399](angular-ui/bootstrap/issues/2399), [#2854](angular-ui/bootstrap/issues/2854))
* **typeahead:** add focus-first option ([35d0cc1d](angular-ui/bootstrap/commit/35d0cc1d57302883840f7ad54a03918ae2df001d), closes [#908](angular-ui/bootstrap/issues/908), [#2916](angular-ui/bootstrap/issues/2916))
## Breaking Changes
* `tooltip-trigger` and `popover-trigger` are no longer watched
attributes.
([a65bea95](angular-ui/bootstrap/commit/a65bea95338802b026fd213805b095b5a0b5b393))
This affects both popovers and tooltips. The *triggers are now set up
once* and can no longer be changed after initialization.
* `dropdown` and `dropdown-toggle` are attribute-only directives. ([192768e1](angular-ui/bootstrap/commit/192768e109b5c4a50c7dcd320e09d05fedd4298a))
Before:
```html
<button class="dropdown-toggle" ...>
```
After:
```html
<button class="dropdown-toggle" dropdown-toggle ...>
```
# 0.11.2 (2014-09-26)
Revert breaking change in **dropdown** ([1a998c4](http://github.com/angular-ui/bootstrap/commit/1a998c4))
# 0.11.1 (2014-09-26)
## Features
- **modal:**
- add backdropClass option, similar to windowClass option ([353e6127](http://github.com/angular-ui/bootstrap/commit/353e6127))
- support alternative controllerAs syntax ([8d7c2a26](http://github.com/angular-ui/bootstrap/commit/8d7c2a26))
- allow templateUrl to be a function ([990015fb](http://github.com/angular-ui/bootstrap/commit/990015fb))
## Bug Fixes
- **alert:**
- correct binding of alert type class ([aa188aec](http://github.com/angular-ui/bootstrap/commit/aa188aec))
- **dateparser:**
- do not parse if no format specified ([42cc3f26](http://github.com/angular-ui/bootstrap/commit/42cc3f26))
- **datepicker:**
- correct `datepicker-mode` binding for popup ([63ae06c9](http://github.com/angular-ui/bootstrap/commit/63ae06c9))
- memory leak fix for datepicker ([08c150e1](http://github.com/angular-ui/bootstrap/commit/08c150e1))
- **dropdown:**
- close after selecting an item ([3ac3b487](http://github.com/angular-ui/bootstrap/commit/3ac3b487))
- remove `C` restrictions to avoid conflicts ([7512b93f](http://github.com/angular-ui/bootstrap/commit/7512b93f))
- **modal:**
- allow modal.{dismiss,close} to be called again ([1590920c](http://github.com/angular-ui/bootstrap/commit/1590920c))
- add a work-around for transclusion scope ([0b31e865](http://github.com/angular-ui/bootstrap/commit/0b31e865))
- allow in-lined controller-as controllers ([79105368](http://github.com/angular-ui/bootstrap/commit/79105368))
- respect autofocus on child elements ([e62ab94a](http://github.com/angular-ui/bootstrap/commit/e62ab94a))
- controllerAs not checked ([7b7cdf84](http://github.com/angular-ui/bootstrap/commit/7b7cdf84))
- **tabs:**
- remove leading newline from a template ([a708fe6d](http://github.com/angular-ui/bootstrap/commit/a708fe6d))
- **typeahead:**
- timeout cancellation when deleting characters ([5dc57927](http://github.com/angular-ui/bootstrap/commit/5dc57927))
- allow multiple line expression ([c7db0df4](http://github.com/angular-ui/bootstrap/commit/c7db0df4))
- replace ng-if with ng-show in matches popup ([a0be450d](http://github.com/angular-ui/bootstrap/commit/a0be450d))
# 0.11.0 (2014-05-01)
## Features
- **accordion:**
- support `is-disabled` state ([9c43ae7c](http://github.com/angular-ui/bootstrap/commit/9c43ae7c))
- **alert:**
- add WAI-ARIA markup ([9a2638bf](http://github.com/angular-ui/bootstrap/commit/9a2638bf))
- **button:**
- allow uncheckable radio button ([82df4fb1](http://github.com/angular-ui/bootstrap/commit/82df4fb1))
- **carousel:**
- Support swipe for touchscreen devices ([85140f84](http://github.com/angular-ui/bootstrap/commit/85140f84))
- **dateParser:**
- add `dateParser` service ([bd2ae0ee](http://github.com/angular-ui/bootstrap/commit/bd2ae0ee))
- **datepicker:**
- add `datepicker-mode`, `init-date` & today hint ([7f4b40eb](http://github.com/angular-ui/bootstrap/commit/7f4b40eb))
- make widget accessible ([2423f6d4](http://github.com/angular-ui/bootstrap/commit/2423f6d4))
- full six-week calendar ([b0b14343](http://github.com/angular-ui/bootstrap/commit/b0b14343))
- **dropdown:**
- add WAI-ARIA attributes ([22ebd230](http://github.com/angular-ui/bootstrap/commit/22ebd230))
- focus toggle element when opening or closing with Esc` ([f715d052](http://github.com/angular-ui/bootstrap/commit/f715d052))
- **dropdownToggle:**
- support programmatic trigger & toggle callback ([ae31079c](http://github.com/angular-ui/bootstrap/commit/ae31079c))
- add support for `escape` key ([1417c548](http://github.com/angular-ui/bootstrap/commit/1417c548))
- **modal:**
- support custom template for modal window ([96def3d6](http://github.com/angular-ui/bootstrap/commit/96def3d6))
- support modal window sizes ([976f6083](http://github.com/angular-ui/bootstrap/commit/976f6083))
- improve accessibility - add role='dialog' ([60cee9dc](http://github.com/angular-ui/bootstrap/commit/60cee9dc))
- **pagination:**
- plug into `ngModel` controller ([d65901cf](http://github.com/angular-ui/bootstrap/commit/d65901cf))
- **progressbar:**
- make widget accessible ([9dfe3157](http://github.com/angular-ui/bootstrap/commit/9dfe3157))
- **rating:**
- plug into `ngModel` controller ([47e227f6](http://github.com/angular-ui/bootstrap/commit/47e227f6))
- make widget accessible ([4f56e60e](http://github.com/angular-ui/bootstrap/commit/4f56e60e))
- **tooltip:**
- support more positioning options ([3704db9a](http://github.com/angular-ui/bootstrap/commit/3704db9a))
- **typeahead:**
- add WAI-ARIA markup ([5ca23e97](http://github.com/angular-ui/bootstrap/commit/5ca23e97))
- add `aria-owns` & `aria-activedescendant` roles ([4c76a858](http://github.com/angular-ui/bootstrap/commit/4c76a858))
## Bug Fixes
- **alert:**
- use interpolation for type attribute ([f0a129ad](http://github.com/angular-ui/bootstrap/commit/f0a129ad))
- add `alert-dismissable` class ([794954af](http://github.com/angular-ui/bootstrap/commit/794954af))
- **carousel:**
- correct glyphicon ([3b6ab25b](http://github.com/angular-ui/bootstrap/commit/3b6ab25b))
- **datepicker:**
- remove unneeded date creation ([68cb2e5a](http://github.com/angular-ui/bootstrap/commit/68cb2e5a))
- `Today` button should not set time ([e1993491](http://github.com/angular-ui/bootstrap/commit/e1993491))
- mark input field as invalid if the date is invalid ([467dd159](http://github.com/angular-ui/bootstrap/commit/467dd159))
- rename `dateFormat` to `datepickerPopup` in datepickerPopupConfig ([93da30d5](http://github.com/angular-ui/bootstrap/commit/93da30d5))
- parse input using dateParser ([e0eb1bce](http://github.com/angular-ui/bootstrap/commit/e0eb1bce))
- **dropdown:**
- use $animate for adding and removing classes ([e8d5fefc](http://github.com/angular-ui/bootstrap/commit/e8d5fefc))
- unbind toggle element event on scope destroy ([890e2d37](http://github.com/angular-ui/bootstrap/commit/890e2d37))
- do not call `on-toggle` initially ([004dd1de](http://github.com/angular-ui/bootstrap/commit/004dd1de))
- ensure `on-toggle` works when `is-open` is not used ([06ad3bd5](http://github.com/angular-ui/bootstrap/commit/06ad3bd5))
- **modal:**
- destroy modal scope after animation end ([dfc36fd9](http://github.com/angular-ui/bootstrap/commit/dfc36fd9))
- backdrop z-index when stacking modals ([94a7f593](http://github.com/angular-ui/bootstrap/commit/94a7f593))
- give a reason of rejection when escape key pressed ([cb31b875](http://github.com/angular-ui/bootstrap/commit/cb31b875))
- prevent default event when closing via escape key ([da951222](http://github.com/angular-ui/bootstrap/commit/da951222))
- toggle 'modal-open' class after animation ([4d641ca7](http://github.com/angular-ui/bootstrap/commit/4d641ca7))
- **pagination:**
- take maxSize defaults into account ([a294c87f](http://github.com/angular-ui/bootstrap/commit/a294c87f))
- **position:**
- remove deprecated body scrollTop and scrollLeft ([1ba07c1b](http://github.com/angular-ui/bootstrap/commit/1ba07c1b))
- **progressbar:**
- allow fractional values for bar width ([0daa7a74](http://github.com/angular-ui/bootstrap/commit/0daa7a74))
- number filter in bar template and only for percent ([378a9337](http://github.com/angular-ui/bootstrap/commit/378a9337))
- **tabs:**
- fire deselect before select callback ([7474c47b](http://github.com/angular-ui/bootstrap/commit/7474c47b))
- use interpolation for type attribute ([83ceb78a](http://github.com/angular-ui/bootstrap/commit/83ceb78a))
- remove `tabbable` class required for left/right tabs ([19468331](http://github.com/angular-ui/bootstrap/commit/19468331))
- **timepicker:**
- evaluate correctly the `readonly-input` attribute ([f9b6c496](http://github.com/angular-ui/bootstrap/commit/f9b6c496))
- **tooltip:**
- animation causes tooltip to hide on show ([2b429f5d](http://github.com/angular-ui/bootstrap/commit/2b429f5d))
- **typeahead:**
- correctly handle append to body attribute ([10785736](http://github.com/angular-ui/bootstrap/commit/10785736))
- correctly higlight numeric matches ([09678b12](http://github.com/angular-ui/bootstrap/commit/09678b12))
- loading callback updates after blur ([6a830116](http://github.com/angular-ui/bootstrap/commit/6a830116))
- incompatibility with ng-focus ([d0024931](http://github.com/angular-ui/bootstrap/commit/d0024931))
## Breaking Changes
- **alert:**
Use interpolation for type attribute.
Before:
```html
<alert type="'info'" ...></alert >
```
or
```html
<alert type="alert.type" ...></alert >
```
After:
```html
<alert type="info" ...></alert >
```
or
```html
<alert type="{{alert.type}}" ...></alert >
```
- **datepicker:**
`show-weeks` is no longer a watched attribute
`*-format` attributes have been renamed to `format-*`
`min` attribute has been renamed to `min-date`
`max` attribute has been renamed to `max-date`
- **pagination:**
Both `pagination` and `pager` are now integrated with `ngModelController`.
* `page` is replaced from `ng-model`.
* `on-select-page` is removed since `ng-change` can now be used.
Before:
```html
<pagination page="current" on-select-page="changed(page)" ...></pagination>
```
After:
```html
<pagination ng-model="current" ng-change="changed()" ...></pagination>
```
- **rating:**
`rating` is now integrated with `ngModelController`.
* `value` is replaced from `ng-model`.
Before:
```html
<rating value="rate" ...></rating>
```
After:
```html
<rating ng-model="rate" ...></rating>
```
- **tabs:**
Use interpolation for type attribute.
Before:
```html
<tabset type="'pills'" ...></tabset >
<!-- or -->
<tabset type="navtype" ...></tabset>
```
After:
```html
<tabset type="pills" ...></tabset>
<!-- or -->
<tabset type="{{navtype}}" ...></tabset>
```
# 0.10.0 (2014-01-13)
_This release adds AngularJS 1.2 support_
## Features
- **modal:**
- expose dismissAll on $modalStack ([bc8d21c1](http://github.com/angular-ui/bootstrap/commit/bc8d21c1))
## Bug Fixes
- **datepicker:**
- evaluate `show-weeks` from `datepicker-options` ([92c1715f](http://github.com/angular-ui/bootstrap/commit/92c1715f))
- **modal:**
- leaking watchers due to scope re-use ([0754ad7b](http://github.com/angular-ui/bootstrap/commit/0754ad7b))
- support close animation ([1933488c](http://github.com/angular-ui/bootstrap/commit/1933488c))
- **timepicker:**
- add correct type for meridian button ([bcf39efe](http://github.com/angular-ui/bootstrap/commit/bcf39efe))
- **tooltip:**
- performance and scope fixes ([c0df3201](http://github.com/angular-ui/bootstrap/commit/c0df3201))
# 0.9.0 (2013-12-28)
_This release adds Bootstrap3 support_
## Features
- **accordion:**
- convert to bootstrap3 panel styling ([458a9bd3](http://github.com/angular-ui/bootstrap/commit/458a9bd3))
- **carousel:**
- some changes for Bootstrap3 ([1f632b65](http://github.com/angular-ui/bootstrap/commit/1f632b65))
- **collapse:**
- make collapse work with bootstrap3 ([517dff6e](http://github.com/angular-ui/bootstrap/commit/517dff6e))
- **datepicker:**
- update to Bootstrap 3 ([37684330](http://github.com/angular-ui/bootstrap/commit/37684330))
- **modal:**
- added bootstrap3 support ([444c488d](http://github.com/angular-ui/bootstrap/commit/444c488d))
- **pagination:**
- support bootstrap3 ([3db699d7](http://github.com/angular-ui/bootstrap/commit/3db699d7))
- **progressbar:**
- update to bootstrap3 ([5bcff623](http://github.com/angular-ui/bootstrap/commit/5bcff623))
- **rating:**
- update rating to bootstrap3 ([7e60284e](http://github.com/angular-ui/bootstrap/commit/7e60284e))
- **tabs:**
- add nav-justified ([3199dd88](http://github.com/angular-ui/bootstrap/commit/3199dd88))
- **timepicker:**
- restyled for bootstrap 3 ([6724a721](http://github.com/angular-ui/bootstrap/commit/6724a721))
- **typeahead:**
- update to Bootstrap 3 ([eadf934a](http://github.com/angular-ui/bootstrap/commit/eadf934a))
## Bug Fixes
- **alert:**
- update template to Bootstrap 3 ([dfc3b0bd](http://github.com/angular-ui/bootstrap/commit/dfc3b0bd))
- **collapse:**
- Prevent consecutive transitions & tidy up code ([b0032d68](http://github.com/angular-ui/bootstrap/commit/b0032d68))
- fixes after rebase ([dc02ad1d](http://github.com/angular-ui/bootstrap/commit/dc02ad1d))
- **rating:**
- user glyhicon classes ([d221d517](http://github.com/angular-ui/bootstrap/commit/d221d517))
- **timepicker:**
- fix look with bootstrap3 ([9613b61b](http://github.com/angular-ui/bootstrap/commit/9613b61b))
- **tooltip:**
- re-position tooltip after draw ([a99b3608](http://github.com/angular-ui/bootstrap/commit/a99b3608))
# 0.8.0 (2013-12-28)
## Features
- **datepicker:**
- option whether to display button bar in popup ([4d158e0d](http://github.com/angular-ui/bootstrap/commit/4d158e0d))
- **modal:**
- add modal-open class to body on modal open ([e76512fa](http://github.com/angular-ui/bootstrap/commit/e76512fa))
- **progressbar:**
- add `max` attribute & support transclusion ([365573ab](http://github.com/angular-ui/bootstrap/commit/365573ab))
- **timepicker:**
- default meridian labels based on locale ([8b1ab79a](http://github.com/angular-ui/bootstrap/commit/8b1ab79a))
- **typeahead:**
- add typeahead-append-to-body option ([dd8eac22](http://github.com/angular-ui/bootstrap/commit/dd8eac22))
## Bug Fixes
- **accordion:**
- correct `is-open` handling for dynamic groups ([9ec21286](http://github.com/angular-ui/bootstrap/commit/9ec21286))
- **carousel:**
- cancel timer on scope destruction ([5b9d929c](http://github.com/angular-ui/bootstrap/commit/5b9d929c))
- cancel goNext on scope destruction ([7515df45](http://github.com/angular-ui/bootstrap/commit/7515df45))
- **collapse:**
- dont animate height changes from 0 to 0 ([81e014a8](http://github.com/angular-ui/bootstrap/commit/81e014a8))
- **datepicker:**
- set default zero time after no date selected ([93cd0df8](http://github.com/angular-ui/bootstrap/commit/93cd0df8))
- fire `ngChange` on today/clear button press ([6b1c68fb](http://github.com/angular-ui/bootstrap/commit/6b1c68fb))
- remove datepicker's popup on scope destroy ([48955d69](http://github.com/angular-ui/bootstrap/commit/48955d69))
- remove edge case position updates ([1fbcb5d6](http://github.com/angular-ui/bootstrap/commit/1fbcb5d6))
- **modal:**
- put backdrop in before window ([d64f4a97](http://github.com/angular-ui/bootstrap/commit/d64f4a97))
- grab reference to body when it is needed in lieu of when the factory is created ([dd415a98](http://github.com/angular-ui/bootstrap/commit/dd415a98))
- focus freshly opened modal ([709e679c](http://github.com/angular-ui/bootstrap/commit/709e679c))
- properly animate backdrops on each modal opening ([672a557a](http://github.com/angular-ui/bootstrap/commit/672a557a))
- **tabs:**
- make nested tabs work ([c9acebbe](http://github.com/angular-ui/bootstrap/commit/c9acebbe))
- **tooltip:**
- update tooltip content when empty ([60515ae1](http://github.com/angular-ui/bootstrap/commit/60515ae1))
- support IE8 ([5dd98238](http://github.com/angular-ui/bootstrap/commit/5dd98238))
- unbind element events on scope destroy ([3fe7aa8c](http://github.com/angular-ui/bootstrap/commit/3fe7aa8c))
- respect animate attribute ([54e614a8](http://github.com/angular-ui/bootstrap/commit/54e614a8))
## Breaking Changes
- **progressbar:**
The onFull/onEmpty handlers & auto/stacked types have been removed.
To migrate your code change your markup like below.
Before:
```html
<progress percent="var" class="progress-warning"></progress>
```
After:
```html
<progressbar value="var" type="warning"></progressbar>
```
and for stacked instead of passing array/objects you can do:
```html
<progress><bar ng-repeat="obj in objs" value="obj.var" type="{{obj.type}}"></bar></progress>
```
# 0.7.0 (2013-11-22)
## Features
- **datepicker:**
- add i18n support for bar buttons in popup ([c6ba8d7f](http://github.com/angular-ui/bootstrap/commit/c6ba8d7f))
- dynamic date format for popup ([aa3eaa91](http://github.com/angular-ui/bootstrap/commit/aa3eaa91))
- datepicker-append-to-body attribute ([0cdc4609](http://github.com/angular-ui/bootstrap/commit/0cdc4609))
- **dropdownToggle:**
- disable dropdown when it has the disabled class ([104bdd1b](http://github.com/angular-ui/bootstrap/commit/104bdd1b))
- **tooltip:**
- add ability to enable / disable tooltip ([5d9bd058](http://github.com/angular-ui/bootstrap/commit/5d9bd058))
## Bug Fixes
- **accordion:**
- assign `is-open` to correct scope ([157f614a](http://github.com/angular-ui/bootstrap/commit/157f614a))
- **collapse:**
- remove element height watching ([a72c635c](http://github.com/angular-ui/bootstrap/commit/a72c635c))
- add the "in" class for expanded panels ([9eca35a8](http://github.com/angular-ui/bootstrap/commit/9eca35a8))
- **datepicker:**
- some IE8 compatibility improvements ([4540476f](http://github.com/angular-ui/bootstrap/commit/4540476f))
- set popup initial position in append-to-body case ([78a1e9d7](http://github.com/angular-ui/bootstrap/commit/78a1e9d7))
- properly handle showWeeks config option ([570dba90](http://github.com/angular-ui/bootstrap/commit/570dba90))
- **modal:**
- correctly close modals with no backdrop ([e55c2de3](http://github.com/angular-ui/bootstrap/commit/e55c2de3))
- **pagination:**
- fix altering of current page caused by totals change ([81164dae](http://github.com/angular-ui/bootstrap/commit/81164dae))
- handle extreme values for `total-items` ([8ecf93ed](http://github.com/angular-ui/bootstrap/commit/8ecf93ed))
- **position:**
- correct positioning for SVG elements ([968e5407](http://github.com/angular-ui/bootstrap/commit/968e5407))
- **tabs:**
- initial tab selection ([a08173ec](http://github.com/angular-ui/bootstrap/commit/a08173ec))
- **timepicker:**
- use html5 for input elements ([53709f0f](http://github.com/angular-ui/bootstrap/commit/53709f0f))
- **tooltip:**
- restore html-unsafe compatibility with AngularJS 1.2 ([08d8b21d](http://github.com/angular-ui/bootstrap/commit/08d8b21d))
- hide tooltips when content becomes empty ([cf5c27ae](http://github.com/angular-ui/bootstrap/commit/cf5c27ae))
- tackle DOM node and event handlers leak ([0d810acd](http://github.com/angular-ui/bootstrap/commit/0d810acd))
- **typeahead:**
- do not set editable error when input is empty ([006986db](http://github.com/angular-ui/bootstrap/commit/006986db))
- remove popup flickering ([dde804b6](http://github.com/angular-ui/bootstrap/commit/dde804b6))
- don't show matches if an element is not focused ([d1f94530](http://github.com/angular-ui/bootstrap/commit/d1f94530))
- fix loading callback when deleting characters ([0149eff6](http://github.com/angular-ui/bootstrap/commit/0149eff6))
- prevent accidental form submission on ENTER ([253c49ff](http://github.com/angular-ui/bootstrap/commit/253c49ff))
- evaluate matches source against a correct scope ([fd21214d](http://github.com/angular-ui/bootstrap/commit/fd21214d))
- support IE8 ([0e9f9980](http://github.com/angular-ui/bootstrap/commit/0e9f9980))
# 0.6.0 (2013-09-08)
## Features
- **modal:**
- rewrite $dialog as $modal ([d7a48523](http://github.com/angular-ui/bootstrap/commit/d7a48523))
- add support for custom window settings ([015625d1](http://github.com/angular-ui/bootstrap/commit/015625d1))
- expose $close and $dismiss options on modal's scope ([8d153acb](http://github.com/angular-ui/bootstrap/commit/8d153acb))
- **pagination:**
- `total-items` & optional `items-per-page` API ([e55d9063](http://github.com/angular-ui/bootstrap/commit/e55d9063))
- **rating:**
- add support for custom icons per instance ([20ab01ad](http://github.com/angular-ui/bootstrap/commit/20ab01ad))
- **timepicker:**
- plug into `ngModel` controller ([b08e993f](http://github.com/angular-ui/bootstrap/commit/b08e993f))
## Bug Fixes
- **carousel:**
- correct reflow triggering on FFox and Safari ([d34f2de1](http://github.com/angular-ui/bootstrap/commit/d34f2de1))
- **datepicker:**
- correctly manage focus without jQuery present ([d474824b](http://github.com/angular-ui/bootstrap/commit/d474824b))
- compatibility with angular 1.1.5 and no jquery ([bf30898d](http://github.com/angular-ui/bootstrap/commit/bf30898d))
- use $setViewValue for inner changes ([dd99f35d](http://github.com/angular-ui/bootstrap/commit/dd99f35d))
- **modal:**
- insert backdrop before modal window ([d870f212](http://github.com/angular-ui/bootstrap/commit/d870f212))
- ie8 fix after $modal rewrite ([ff9d969e](http://github.com/angular-ui/bootstrap/commit/ff9d969e))
- opening a modal should not change default options ([82532d1b](http://github.com/angular-ui/bootstrap/commit/82532d1b))
- backdrop should cover previously opened modals ([7fce2fe8](http://github.com/angular-ui/bootstrap/commit/7fce2fe8))
- allow replacing object with default options ([8e7fbf06](http://github.com/angular-ui/bootstrap/commit/8e7fbf06))
- **position:**
- fallback for IE8's scrollTop/Left for offset ([9aecd4ed](http://github.com/angular-ui/bootstrap/commit/9aecd4ed))
- **tabs:**
- add DI array-style annotations ([aac4a0dd](http://github.com/angular-ui/bootstrap/commit/aac4a0dd))
- evaluate `vertical` on parent scope ([9af6f96e](http://github.com/angular-ui/bootstrap/commit/9af6f96e))
- **timepicker:**
- add type attribute for meridian button ([1f89fd4b](http://github.com/angular-ui/bootstrap/commit/1f89fd4b))
- **tooltip:**
- remove placement='mouse' option ([17163c22](http://github.com/angular-ui/bootstrap/commit/17163c22))
- **typeahead:**
- fix label rendering for equal model and items names ([5de71216](http://github.com/angular-ui/bootstrap/commit/5de71216))
- set validity flag for non-editable inputs ([366e0c8a](http://github.com/angular-ui/bootstrap/commit/366e0c8a))
- plug in front of existing parsers ([80cef614](http://github.com/angular-ui/bootstrap/commit/80cef614))
- highlight return match if no query ([45dd9be1](http://github.com/angular-ui/bootstrap/commit/45dd9be1))
- keep pop-up on clicking input ([5f9e270d](http://github.com/angular-ui/bootstrap/commit/5f9e270d))
- remove dependency on ng-bind-html-unsafe ([75893393](http://github.com/angular-ui/bootstrap/commit/75893393))
## Breaking Changes
- **modal:**
* `$dialog` service was refactored into `$modal`
* `modal` directive was removed - use the `$modal` service instead
Check the documentation for the `$modal` service to migrate from `$dialog`
- **pagination:**
API has undergone some changes in order to be easier to use.
* `current-page` is replaced from `page`.
* Number of pages is not defined by `num-pages`, but from `total-items` &
`items-per-page` instead. If `items-per-page` is missing, default is 10.
* `num-pages` still exists but is just readonly.
Before:
```html
<pagination num-pages="10" ...></pagination>
```
After:
```html
<pagination total-items="100" ...></pagination>
```
- **tooltip:**
The placment='mouse' is gone with no equivalent
# 0.5.0 (2013-08-04)
## Features
- **buttons:**
- support dynamic true / false values in btn-checkbox ([3e30cd94](http://github.com/angular-ui/bootstrap/commit/3e30cd94))
- **datepicker:**
- `ngModelController` plug & new `datepickerPopup` ([dab18336](http://github.com/angular-ui/bootstrap/commit/dab18336))
- **rating:**
- added onHover and onLeave. ([5b1115e3](http://github.com/angular-ui/bootstrap/commit/5b1115e3))
- **tabs:**
- added onDeselect callback, used similarly as onSelect ([fe47c9bb](http://github.com/angular-ui/bootstrap/commit/fe47c9bb))
- add the ability to set the direction of the tabs ([220e7b60](http://github.com/angular-ui/bootstrap/commit/220e7b60))
- **typeahead:**
- support custom templates for matched items ([e2238174](http://github.com/angular-ui/bootstrap/commit/e2238174))
- expose index to custom templates ([5ffae83d](http://github.com/angular-ui/bootstrap/commit/5ffae83d))
## Bug Fixes
- **datepicker:**
- handle correctly `min`/`max` when cleared ([566bdd16](http://github.com/angular-ui/bootstrap/commit/566bdd16))
- add type attribute for buttons ([25caf5fb](http://github.com/angular-ui/bootstrap/commit/25caf5fb))
- **pagination:**
- handle `currentPage` number as string ([b1fa7bb8](http://github.com/angular-ui/bootstrap/commit/b1fa7bb8))
- use interpolation for text attributes ([f45815cb](http://github.com/angular-ui/bootstrap/commit/f45815cb))
- **popover:**
- don't unbind event handlers created by other directives ([56f624a2](http://github.com/angular-ui/bootstrap/commit/56f624a2))
- correctly position popovers appended to body ([93a82af0](http://github.com/angular-ui/bootstrap/commit/93a82af0))
- **rating:**
- evaluate `max` attribute on parent scope ([60619d51](http://github.com/angular-ui/bootstrap/commit/60619d51))
- **tabs:**
- make tab contents be correctly connected to parent (#524) ([be7ecff0](http://github.com/angular-ui/bootstrap/commit/be7ecff0))
- Make tabset template correctly use tabset attributes (#584) ([8868f236](http://github.com/angular-ui/bootstrap/commit/8868f236))
- fix tab content compiling wrong (Closes #599, #631, #574) ([224bc2f5](http://github.com/angular-ui/bootstrap/commit/224bc2f5))
- make tabs added with active=true be selected ([360cd5ca](http://github.com/angular-ui/bootstrap/commit/360cd5ca))
- if tab is active at start, always select it ([ba1f741d](http://github.com/angular-ui/bootstrap/commit/ba1f741d))
- **timepicker:**
- prevent date change ([ee741707](http://github.com/angular-ui/bootstrap/commit/ee741707))
- added wheel event to enable mousewheel on Firefox ([8dc92afa](http://github.com/angular-ui/bootstrap/commit/8dc92afa))
- **tooltip:**
- fix positioning inside scrolling element ([63ae7e12](http://github.com/angular-ui/bootstrap/commit/63ae7e12))
- triggers should be local to tooltip instances ([58e8ef4f](http://github.com/angular-ui/bootstrap/commit/58e8ef4f))
- correctly handle initial events unbinding ([4fd5bf43](http://github.com/angular-ui/bootstrap/commit/4fd5bf43))
- bind correct 'hide' event handler ([d50b0547](http://github.com/angular-ui/bootstrap/commit/d50b0547))
- **typeahead:**
- play nicelly with existing formatters ([d2df0b35](http://github.com/angular-ui/bootstrap/commit/d2df0b35))
- properly render initial input value ([c4e169cb](http://github.com/angular-ui/bootstrap/commit/c4e169cb))
- separate text field rendering and drop down rendering ([ea1e858a](http://github.com/angular-ui/bootstrap/commit/ea1e858a))
- fixed waitTime functionality ([90a8aa79](http://github.com/angular-ui/bootstrap/commit/90a8aa79))
- correctly close popup on match selection ([624fd5f5](http://github.com/angular-ui/bootstrap/commit/624fd5f5))
## Breaking Changes
- **pagination:**
The 'first-text', 'previous-text', 'next-text' and 'last-text'
attributes are now interpolated.
To migrate your code, remove quotes for constant attributes and/or
interpolate scope variables.
Before:
```html
<pagination first-text="'<<'" ...></pagination>
```
and/or
```html
$scope.var1 = '<<';
<pagination first-text="var1" ...></pagination>
```
After:
```html
<pagination first-text="<<" ...></pagination>
```
and/or
```html
$scope.var1 = '<<';
<pagination first-text="{{var1}}" ...></pagination>
```
# 0.4.0 (2013-06-24)
## Features
- **buttons:**
- support dynamic values in btn-radio ([e8c5b548](http://github.com/angular-ui/bootstrap/commit/e8c5b548))
- **carousel:**
- add option to prevent pause ([5f895c13](http://github.com/angular-ui/bootstrap/commit/5f895c13))
- **datepicker:**
- add datepicker directive ([30a00a07](http://github.com/angular-ui/bootstrap/commit/30a00a07))
- **pagination:**
- option for different mode when maxSize ([a023d082](http://github.com/angular-ui/bootstrap/commit/a023d082))
- add pager directive ([d9526475](http://github.com/angular-ui/bootstrap/commit/d9526475))
- **tabs:**
- Change directive name, add features ([c5326595](http://github.com/angular-ui/bootstrap/commit/c5326595))
- support disabled state ([2b78dd16](http://github.com/angular-ui/bootstrap/commit/2b78dd16))
- add support for vertical option ([88d17a75](http://github.com/angular-ui/bootstrap/commit/88d17a75))
- add support for other navigation types, like 'pills' ([53e0a39f](http://github.com/angular-ui/bootstrap/commit/53e0a39f))
- **timepicker:**
- add timepicker directive ([9bc5207b](http://github.com/angular-ui/bootstrap/commit/9bc5207b))
- **tooltip:**
- add mouse placement option ([ace7bc60](http://github.com/angular-ui/bootstrap/commit/ace7bc60))
- add *-append-to-body attribute ([d0896263](http://github.com/angular-ui/bootstrap/commit/d0896263))
- add custom trigger support ([dfa53155](http://github.com/angular-ui/bootstrap/commit/dfa53155))
- **typeahead:**
- support typeahead-on-select callback ([91ac17c9](http://github.com/angular-ui/bootstrap/commit/91ac17c9))
- support wait-ms option ([7f35a3f2](http://github.com/angular-ui/bootstrap/commit/7f35a3f2))
## Bug Fixes
- **accordion:**
- allow accordion heading directives as attributes. ([25f6e55c](http://github.com/angular-ui/bootstrap/commit/25f6e55c))
- **carousel:**
- do not allow user to change slide if transitioning ([1d19663f](http://github.com/angular-ui/bootstrap/commit/1d19663f))
- make slide 'active' binding optional ([17d6c3b5](http://github.com/angular-ui/bootstrap/commit/17d6c3b5))
- fix error with deleting multiple slides at once ([3fcb70f0](http://github.com/angular-ui/bootstrap/commit/3fcb70f0))
- **dialog:**
- remove dialogOpenClass to get in line with v2.3 ([f009b23f](http://github.com/angular-ui/bootstrap/commit/f009b23f))
- **pagination:**
- bind *-text attributes ([e1bff6b7](http://github.com/angular-ui/bootstrap/commit/e1bff6b7))
- **progressbar:**
- user `percent` attribute instead of `value`. ([58efec80](http://github.com/angular-ui/bootstrap/commit/58efec80))
- **tooltip:**
- fix positioning error when appendToBody is set to true ([76fee1f9](http://github.com/angular-ui/bootstrap/commit/76fee1f9))
- close tooltips appended to body on location change ([041261b5](http://github.com/angular-ui/bootstrap/commit/041261b5))
- tooltips will hide on scope.$destroy ([3e5a58e5](http://github.com/angular-ui/bootstrap/commit/3e5a58e5))
- support of custom $interpolate.startSymbol ([88c94ee6](http://github.com/angular-ui/bootstrap/commit/88c94ee6))
- make sure tooltip scope is evicted from cache ([9246905a](http://github.com/angular-ui/bootstrap/commit/9246905a))
- **typeahead:**
- return focus to the input after selecting a suggestion ([04a21e33](http://github.com/angular-ui/bootstrap/commit/04a21e33))
## Breaking Changes
- **pagination:**
The 'first-text', 'previous-text', 'next-text' and 'last-text'
attributes are now binded to parent scope.
To migrate your code, surround the text of these attributes with quotes.
Before:
```html
<pagination first-text="<<"></pagination>
```
After:
```html
<pagination first-text="'<<'"></pagination>
```
- **progressbar:**
The 'value' is replaced by 'percent'.
Before:
```html
<progress value="..."></progress>
```
After:
```html
<progress percent="..."></progress>
```
- **tabs:**
The 'tabs' directive has been renamed to 'tabset', and
the 'pane' directive has been renamed to 'tab'.
To migrate your code, follow the example below.
Before:
```html
<tabs>
<pane heading="one">
First Content
</pane>
<pane ng-repeat="apple in basket" heading="{{apple.heading}}">
{{apple.content}}
</pane>
</tabs>
```
After:
```html
<tabset>
<tab heading="one">
First Content
</tab>
<tab ng-repeat="apple in basket" heading="{{apple.heading}}">
{{apple.content}}
</tab>
</tabset>
```
# 0.3.0 (2013-04-30)
## Features
- **progressbar:**
- add progressbar directive ([261f2072](https://github.com/angular-ui/bootstrap/commit/261f2072))
- **rating:**
- add rating directive ([6b5e6369](https://github.com/angular-ui/bootstrap/commit/6b5e6369))
- **typeahead:**
- support the editable property ([a40c3fbe](https://github.com/angular-ui/bootstrap/commit/a40c3fbe))
- support typeahead-loading bindable expression ([b58c9c88](https://github.com/angular-ui/bootstrap/commit/b58c9c88))
- **tooltip:**
- added popup-delay option ([a79a2ba8](https://github.com/angular-ui/bootstrap/commit/a79a2ba8))
- added appendToBody to $tooltip ([1ee467f8](https://github.com/angular-ui/bootstrap/commit/1ee467f8))
- added tooltip-html-unsafe directive ([45ed2805](https://github.com/angular-ui/bootstrap/commit/45ed2805))
- support for custom triggers ([b1ba821b](https://github.com/angular-ui/bootstrap/commit/b1ba821b))
## Bug Fixes
- **alert:**
- don't show close button if no close callback specified ([c2645f4a](https://github.com/angular-ui/bootstrap/commit/c2645f4a))
- **carousel:**
- Hide navigation indicators if only one slide ([aedc0565](https://github.com/angular-ui/bootstrap/commit/aedc0565))
- **collapse:**
- remove reference to msTransition for IE10 ([55437b16](https://github.com/angular-ui/bootstrap/commit/55437b16))
- **dialog:**
- set _open to false on init ([dcc9ef31](https://github.com/angular-ui/bootstrap/commit/dcc9ef31))
- close dialog on location change ([474ce52e](https://github.com/angular-ui/bootstrap/commit/474ce52e))
- IE8 fix to not set data() against text nodes ([a6c540e5](https://github.com/angular-ui/bootstrap/commit/a6c540e5))
- fix $apply in progres on $location change ([77e6acb9](https://github.com/angular-ui/bootstrap/commit/77e6acb9))
- **tabs:**
- remove superfluous href from tabs template ([38c1badd](https://github.com/angular-ui/bootstrap/commit/38c1badd))
- **tooltip:**
- fix positioning issues in tooltips and popovers ([6458f487](https://github.com/angular-ui/bootstrap/commit/6458f487))
- **typeahead:**
- close matches popup on click outside typeahead ([acca7dcd](https://github.com/angular-ui/bootstrap/commit/acca7dcd))
- stop keydown event propagation when ESC pressed to discard matches ([22a00cd0](https://github.com/angular-ui/bootstrap/commit/22a00cd0))
- correctly render initial model value ([929a46fa](https://github.com/angular-ui/bootstrap/commit/929a46fa))
- correctly higlight matches if query contains regexp-special chars ([467afcd6](https://github.com/angular-ui/bootstrap/commit/467afcd6))
- fix matches pop-up positioning issues ([74beecdb](https://github.com/angular-ui/bootstrap/commit/74beecdb))
# 0.2.0 (2013-03-03)
## Features
- **dialog:**
- Make $dialog 'resolve' property to work the same way of $routeProvider.when ([739f86f](https://github.com/angular-ui/bootstrap/commit/739f86f))
- **modal:**
- allow global override of modal options ([acaf72b](https://github.com/angular-ui/bootstrap/commit/acaf72b))
- **buttons:**
- add checkbox and radio buttons ([571ccf4](https://github.com/angular-ui/bootstrap/commit/571ccf4))
- **carousel:**
- add slide indicators ([3b677ee](https://github.com/angular-ui/bootstrap/commit/3b677ee))
- **typeahead:**
- add typeahead directive ([6a97da2](https://github.com/angular-ui/bootstrap/commit/6a97da2))
- **accordion:**
- enable HTML in accordion headings ([3afcaa4](https://github.com/angular-ui/bootstrap/commit/3afcaa4))
- **pagination:**
- add first/last link & constant congif options ([0ff0454](https://github.com/angular-ui/bootstrap/commit/0ff0454))
## Bug fixes
- **dialog:**
- update resolve section to new syntax ([1f87486](https://github.com/angular-ui/bootstrap/commit/1f87486))
- $compile entire modal ([7575b3c](https://github.com/angular-ui/bootstrap/commit/7575b3c))
- **tooltip:**
- don't show tooltips if there is no content to show ([030901e](https://github.com/angular-ui/bootstrap/commit/030901e))
- fix placement issues ([a2bbf4d](https://github.com/angular-ui/bootstrap/commit/a2bbf4d))
- **collapse:**
- Avoids fixed height on collapse ([ff5d119](https://github.com/angular-ui/bootstrap/commit/ff5d119))
- **accordion:**
- fix minification issues ([f4da4d6](https://github.com/angular-ui/bootstrap/commit/f4da4d6))
- **typeahead:**
- update inputs value on mapping where label is not derived from the model ([a5f64de](https://github.com/angular-ui/bootstrap/commit/a5f64de))
# 0.1.0 (2013-02-02)
_Very first, initial release_.
## Features
Version `0.1.0` was released with the following directives:
* accordion
* alert
* carousel
* dialog
* dropdownToggle
* modal
* pagination
* popover
* tabs
* tooltip

View file

@ -0,0 +1,44 @@
## Got a question or problem?
Firstly, please go over our FAQ: https://github.com/angular-ui/bootstrap/wiki/FAQ
Please, do not open issues for the general support questions as we want to keep GitHub issues for bug reports and feature requests. You've got much better chances of getting your question answered on [StackOverflow](http://stackoverflow.com/questions/tagged/angular-ui-bootstrap) where maintainers are looking at questions questions tagged with `angular-ui-bootstrap`.
StackOverflow is a much better place to ask questions since:
* there are hundreds of people willing to help on StackOverflow
* questions and answers stay available for public viewing so your question / answer might help someone else
* SO voting system assures that the best answers are prominently visible.
To save your and our time we will be systematically closing all the issues that are request for general support and redirecting people to StackOverflow.
## You think you've found a bug?
Oh, we are ashamed and want to fix it asap! But before fixing a bug we need to reproduce and confirm it. In order to reproduce bugs we will systematically ask you to provide a _minimal_ reproduce scenario using http://plnkr.co/. Having a live reproduce scenario gives us wealth of important information without going back & forth to you with additional questions like:
* version of AngularJS used
* version of this library that you are using
* 3rd-party libraries used, if any
* and most importantly - a use-case that fails
A minimal reproduce scenario using http://plnkr.co/ allows us to quickly confirm a bug (or point out coding problem) as well as confirm that we are fixing the right problem.
We will be insisting on a minimal reproduce scenario in order to save maintainers time and ultimately be able to fix more bugs. Interestingly, from our experience users often find coding problems themselves while preparing a minimal plunk. We understand that sometimes it might be hard to extract essentials bits of code from a larger code-base but we really need to isolate the problem before we can fix it.
The best part is that you don't need to create plunks from scratch - you can use one from our [demo page](http://angular-ui.github.io/bootstrap/).
Unfortunately we are not able to investigate / fix bugs without a minimal reproduce scenario using http://plnkr.co/, so if we don't hear back from you we are going to close an issue that don't have enough info to be reproduced.
## You want to contribute some code?
We are always looking for the quality contributions and will be happy to accept your Pull Requests as long as those adhere to some basic rules:
* Please make sure that your contribution fits well in the project's context:
* we are aiming at rebuilding bootstrap directives in pure AngularJS, without any dependencies on any external JavaScript library;
* the only dependency should be bootstrap CSS and its markup structure;
* directives should be html-agnostic as much as possible which in practice means:
* templates should be referred to using the `templateUrl` property
* it should be easy to change a default template to a custom one
* directives shouldn't manipulate DOM structure directly (when possible)
* Please assure that you are submitting quality code, specifically make sure that:
* your directive has accompanying tests and all the tests are passing; don't hesitate to contact us (angular-ui@googlegroups.com) if you need any help with unit testing
* your PR doesn't break the build; check the Travis-CI build status after opening a PR and push corrective commits if anything goes wrong

View file

@ -0,0 +1,419 @@
/* jshint node: true */
var markdown = require('node-markdown').Markdown;
module.exports = function(grunt) {
grunt.loadNpmTasks('grunt-contrib-watch');
grunt.loadNpmTasks('grunt-contrib-concat');
grunt.loadNpmTasks('grunt-contrib-copy');
grunt.loadNpmTasks('grunt-contrib-jshint');
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-html2js');
grunt.loadNpmTasks('grunt-karma');
grunt.loadNpmTasks('grunt-conventional-changelog');
grunt.loadNpmTasks('grunt-ngdocs');
grunt.loadNpmTasks('grunt-ddescribe-iit');
// Project configuration.
grunt.util.linefeed = '\n';
grunt.initConfig({
ngversion: '1.2.16',
bsversion: '3.1.1',
modules: [],//to be filled in by build task
pkg: grunt.file.readJSON('package.json'),
dist: 'dist',
filename: 'ui-bootstrap',
filenamecustom: '<%= filename %>-custom',
meta: {
modules: 'angular.module("ui.bootstrap", [<%= srcModules %>]);',
tplmodules: 'angular.module("ui.bootstrap.tpls", [<%= tplModules %>]);',
all: 'angular.module("ui.bootstrap", ["ui.bootstrap.tpls", <%= srcModules %>]);',
banner: ['/*',
' * <%= pkg.name %>',
' * <%= pkg.homepage %>\n',
' * Version: <%= pkg.version %> - <%= grunt.template.today("yyyy-mm-dd") %>',
' * License: <%= pkg.license %>',
' */\n'].join('\n')
},
delta: {
docs: {
files: ['misc/demo/index.html'],
tasks: ['after-test']
},
html: {
files: ['template/**/*.html'],
tasks: ['html2js', 'karma:watch:run']
},
js: {
files: ['src/**/*.js'],
//we don't need to jshint here, it slows down everything else
tasks: ['karma:watch:run']
}
},
concat: {
dist: {
options: {
banner: '<%= meta.banner %><%= meta.modules %>\n'
},
src: [], //src filled in by build task
dest: '<%= dist %>/<%= filename %>-<%= pkg.version %>.js'
},
dist_tpls: {
options: {
banner: '<%= meta.banner %><%= meta.all %>\n<%= meta.tplmodules %>\n'
},
src: [], //src filled in by build task
dest: '<%= dist %>/<%= filename %>-tpls-<%= pkg.version %>.js'
}
},
copy: {
demohtml: {
options: {
//process html files with gruntfile config
processContent: grunt.template.process
},
files: [{
expand: true,
src: ['**/*.html'],
cwd: 'misc/demo/',
dest: 'dist/'
}]
},
demoassets: {
files: [{
expand: true,
//Don't re-copy html files, we process those
src: ['**/**/*', '!**/*.html'],
cwd: 'misc/demo',
dest: 'dist/'
}]
}
},
uglify: {
options: {
banner: '<%= meta.banner %>'
},
dist:{
src:['<%= concat.dist.dest %>'],
dest:'<%= dist %>/<%= filename %>-<%= pkg.version %>.min.js'
},
dist_tpls:{
src:['<%= concat.dist_tpls.dest %>'],
dest:'<%= dist %>/<%= filename %>-tpls-<%= pkg.version %>.min.js'
}
},
html2js: {
dist: {
options: {
module: null, // no bundle module for all the html2js templates
base: '.'
},
files: [{
expand: true,
src: ['template/**/*.html'],
ext: '.html.js'
}]
}
},
jshint: {
files: ['Gruntfile.js','src/**/*.js'],
options: {
jshintrc: '.jshintrc'
}
},
karma: {
options: {
configFile: 'karma.conf.js'
},
watch: {
background: true
},
continuous: {
singleRun: true
},
jenkins: {
singleRun: true,
colors: false,
reporters: ['dots', 'junit'],
browsers: ['Chrome', 'ChromeCanary', 'Firefox', 'Opera', '/Users/jenkins/bin/safari.sh']
},
travis: {
singleRun: true,
reporters: ['dots'],
browsers: ['Firefox']
},
coverage: {
preprocessors: {
'src/*/*.js': 'coverage'
},
reporters: ['progress', 'coverage']
}
},
changelog: {
options: {
dest: 'CHANGELOG.md',
templateFile: 'misc/changelog.tpl.md',
github: 'angular-ui/bootstrap'
}
},
shell: {
//We use %version% and evluate it at run-time, because <%= pkg.version %>
//is only evaluated once
'release-prepare': [
'grunt before-test after-test',
'grunt version', //remove "-SNAPSHOT"
'grunt changelog'
],
'release-complete': [
'git commit CHANGELOG.md package.json -m "chore(release): v%version%"',
'git tag %version%'
],
'release-start': [
'grunt version:minor:"SNAPSHOT"',
'git commit package.json -m "chore(release): Starting v%version%"'
]
},
ngdocs: {
options: {
dest: 'dist/docs',
scripts: [
'angular.js',
'<%= concat.dist_tpls.dest %>'
],
styles: [
'docs/css/style.css'
],
navTemplate: 'docs/nav.html',
title: 'ui-bootstrap',
html5Mode: false
},
api: {
src: ['src/**/*.js', 'src/**/*.ngdoc'],
title: 'API Documentation'
}
},
'ddescribe-iit': {
files: [
'src/**/*.spec.js'
]
}
});
//register before and after test tasks so we've don't have to change cli
//options on the goole's CI server
grunt.registerTask('before-test', ['enforce', 'ddescribe-iit', 'jshint', 'html2js']);
grunt.registerTask('after-test', ['build', 'copy']);
//Rename our watch task to 'delta', then make actual 'watch'
//task build things, then start test server
grunt.renameTask('watch', 'delta');
grunt.registerTask('watch', ['before-test', 'after-test', 'karma:watch', 'delta']);
// Default task.
grunt.registerTask('default', ['before-test', 'test', 'after-test']);
grunt.registerTask('enforce', 'Install commit message enforce script if it doesn\'t exist', function() {
if (!grunt.file.exists('.git/hooks/commit-msg')) {
grunt.file.copy('misc/validate-commit-msg.js', '.git/hooks/commit-msg');
require('fs').chmodSync('.git/hooks/commit-msg', '0755');
}
});
//Common ui.bootstrap module containing all modules for src and templates
//findModule: Adds a given module to config
var foundModules = {};
function findModule(name) {
if (foundModules[name]) { return; }
foundModules[name] = true;
function breakup(text, separator) {
return text.replace(/[A-Z]/g, function (match) {
return separator + match;
});
}
function ucwords(text) {
return text.replace(/^([a-z])|\s+([a-z])/g, function ($1) {
return $1.toUpperCase();
});
}
function enquote(str) {
return '"' + str + '"';
}
var module = {
name: name,
moduleName: enquote('ui.bootstrap.' + name),
displayName: ucwords(breakup(name, ' ')),
srcFiles: grunt.file.expand('src/'+name+'/*.js'),
tplFiles: grunt.file.expand('template/'+name+'/*.html'),
tpljsFiles: grunt.file.expand('template/'+name+'/*.html.js'),
tplModules: grunt.file.expand('template/'+name+'/*.html').map(enquote),
dependencies: dependenciesForModule(name),
docs: {
md: grunt.file.expand('src/'+name+'/docs/*.md')
.map(grunt.file.read).map(markdown).join('\n'),
js: grunt.file.expand('src/'+name+'/docs/*.js')
.map(grunt.file.read).join('\n'),
html: grunt.file.expand('src/'+name+'/docs/*.html')
.map(grunt.file.read).join('\n')
}
};
module.dependencies.forEach(findModule);
grunt.config('modules', grunt.config('modules').concat(module));
}
function dependenciesForModule(name) {
var deps = [];
grunt.file.expand('src/' + name + '/*.js')
.map(grunt.file.read)
.forEach(function(contents) {
//Strategy: find where module is declared,
//and from there get everything inside the [] and split them by comma
var moduleDeclIndex = contents.indexOf('angular.module(');
var depArrayStart = contents.indexOf('[', moduleDeclIndex);
var depArrayEnd = contents.indexOf(']', depArrayStart);
var dependencies = contents.substring(depArrayStart + 1, depArrayEnd);
dependencies.split(',').forEach(function(dep) {
if (dep.indexOf('ui.bootstrap.') > -1) {
var depName = dep.trim().replace('ui.bootstrap.','').replace(/['"]/g,'');
if (deps.indexOf(depName) < 0) {
deps.push(depName);
//Get dependencies for this new dependency
deps = deps.concat(dependenciesForModule(depName));
}
}
});
});
return deps;
}
grunt.registerTask('dist', 'Override dist directory', function() {
var dir = this.args[0];
if (dir) { grunt.config('dist', dir); }
});
grunt.registerTask('build', 'Create bootstrap build files', function() {
var _ = grunt.util._;
//If arguments define what modules to build, build those. Else, everything
if (this.args.length) {
this.args.forEach(findModule);
grunt.config('filename', grunt.config('filenamecustom'));
} else {
grunt.file.expand({
filter: 'isDirectory', cwd: '.'
}, 'src/*').forEach(function(dir) {
findModule(dir.split('/')[1]);
});
}
var modules = grunt.config('modules');
grunt.config('srcModules', _.pluck(modules, 'moduleName'));
grunt.config('tplModules', _.pluck(modules, 'tplModules').filter(function(tpls) { return tpls.length > 0;} ));
grunt.config('demoModules', modules
.filter(function(module) {
return module.docs.md && module.docs.js && module.docs.html;
})
.sort(function(a, b) {
if (a.name < b.name) { return -1; }
if (a.name > b.name) { return 1; }
return 0;
})
);
var moduleFileMapping = _.clone(modules, true);
moduleFileMapping.forEach(function (module) {
delete module.docs;
});
grunt.config('moduleFileMapping', moduleFileMapping);
var srcFiles = _.pluck(modules, 'srcFiles');
var tpljsFiles = _.pluck(modules, 'tpljsFiles');
//Set the concat task to concatenate the given src modules
grunt.config('concat.dist.src', grunt.config('concat.dist.src')
.concat(srcFiles));
//Set the concat-with-templates task to concat the given src & tpl modules
grunt.config('concat.dist_tpls.src', grunt.config('concat.dist_tpls.src')
.concat(srcFiles).concat(tpljsFiles));
grunt.task.run(['concat', 'uglify', 'makeModuleMappingFile', 'makeRawFilesJs']);
});
grunt.registerTask('test', 'Run tests on singleRun karma server', function () {
//this task can be executed in 3 different environments: local, Travis-CI and Jenkins-CI
//we need to take settings for each one into account
if (process.env.TRAVIS) {
grunt.task.run('karma:travis');
} else {
var isToRunJenkinsTask = !!this.args.length;
if(grunt.option('coverage')) {
var karmaOptions = grunt.config.get('karma.options'),
coverageOpts = grunt.config.get('karma.coverage');
grunt.util._.extend(karmaOptions, coverageOpts);
grunt.config.set('karma.options', karmaOptions);
}
grunt.task.run(this.args.length ? 'karma:jenkins' : 'karma:continuous');
}
});
grunt.registerTask('makeModuleMappingFile', function () {
var _ = grunt.util._;
var moduleMappingJs = 'dist/assets/module-mapping.json';
var moduleMappings = grunt.config('moduleFileMapping');
var moduleMappingsMap = _.object(_.pluck(moduleMappings, 'name'), moduleMappings);
var jsContent = JSON.stringify(moduleMappingsMap);
grunt.file.write(moduleMappingJs, jsContent);
grunt.log.writeln('File ' + moduleMappingJs.cyan + ' created.');
});
grunt.registerTask('makeRawFilesJs', function () {
var _ = grunt.util._;
var jsFilename = 'dist/assets/raw-files.json';
var genRawFilesJs = require('./misc/raw-files-generator');
genRawFilesJs(grunt, jsFilename, _.flatten(grunt.config('concat.dist_tpls.src')),
grunt.config('meta.banner'));
});
function setVersion(type, suffix) {
var file = 'package.json';
var VERSION_REGEX = /([\'|\"]version[\'|\"][ ]*:[ ]*[\'|\"])([\d|.]*)(-\w+)*([\'|\"])/;
var contents = grunt.file.read(file);
var version;
contents = contents.replace(VERSION_REGEX, function(match, left, center) {
version = center;
if (type) {
version = require('semver').inc(version, type);
}
//semver.inc strips our suffix if it existed
if (suffix) {
version += '-' + suffix;
}
return left + version + '"';
});
grunt.log.ok('Version set to ' + version.cyan);
grunt.file.write(file, contents);
return version;
}
grunt.registerTask('version', 'Set version. If no arguments, it just takes off suffix', function() {
setVersion(this.args[0], this.args[1]);
});
grunt.registerMultiTask('shell', 'run shell commands', function() {
var self = this;
var sh = require('shelljs');
self.data.forEach(function(cmd) {
cmd = cmd.replace('%version%', grunt.file.readJSON('package.json').version);
grunt.log.ok(cmd);
var result = sh.exec(cmd,{silent:true});
if (result.code !== 0) {
grunt.fatal(result.output);
}
});
});
return grunt;
};

View file

@ -0,0 +1,22 @@
The MIT License
Copyright (c) 2012-2014 the AngularUI Team, https://github.com/organizations/angular-ui/teams/291112
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View file

@ -0,0 +1,214 @@
# bootstrap - [AngularJS](http://angularjs.org/) directives specific to [Bootstrap](http://getbootstrap.com)
[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/angular-ui/bootstrap?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
***
[![Build Status](https://secure.travis-ci.org/angular-ui/bootstrap.svg)](http://travis-ci.org/angular-ui/bootstrap)
[![devDependency Status](https://david-dm.org/angular-ui/bootstrap/dev-status.svg?branch=master)](https://david-dm.org/angular-ui/bootstrap#info=devDependencies)
## IMPT - 2015 PLANS AND ANGULAR 1.3 SUPPORT
As of 17 Jan 2015 the project has bought on new maintainers to try and clear through the backlog of Angular 1.3 issues. As you can appreciate this is a **massive** undertaking
by a purely part-time, unpaid volunteer team; so please be patient with us! The milestones are as follows:
* The **0.12.1** milestone will be for bug fixes for the existing Angular 1.2 supported version
* The **0.13.0** milestone will contain issues / PRs that are majorly blocking 1.3 compatibility
* The **0.13.x** milestone will contain issues / PRs that are nice to haves for 1.3 compatibility
* The **1.0** milestone is TBA
* The **Backlog** milestone is nice to haves
* The **Purgatory** Milestone is *"Good luck getting that in"*
The plan is to:
1. **Rapidly Release 0.12.1** - new Maintainers learning the merge and release process
1. **Triage of existing Pull Requests** - into 0.13.0, 0.13.x, Backlog and Purgatory milestones
1. **Triage of existing issues** - into 0.13.0, 0.13.x, Backlog and Purgatory milestones
1. Obligatory - **profit!**
## Demo
Do you want to see directives in action? Visit http://angular-ui.github.io/bootstrap/!
## Installation
Installation is easy as angular-ui-bootstrap has minimal dependencies - only the AngularJS and Bootstrap's CSS are required.
After downloading dependencies (or better yet, referencing them from your favourite CDN) you need to download build version of this project. All the files and their purposes are described here:
https://github.com/angular-ui/bootstrap/tree/gh-pages#build-files
Don't worry, if you are not sure which file to take, opt for `ui-bootstrap-tpls-[version].min.js`.
When you are done downloading all the dependencies and project files the only remaining part is to add dependencies on the `ui.bootstrap` AngularJS module:
```javascript
angular.module('myModule', ['ui.bootstrap']);
```
Project files are also available through your favourite package manager:
* **Bower**: `bower install angular-bootstrap`
* **NuGet**: https://nuget.org/packages/Angular.UI.Bootstrap/
## Support
If you are having problems making some directives work, there are several ways to get help:
* Live help in the IRC (`#angularjs` channel at the `freenode` network). Use this [webchat](https://webchat.freenode.net/) or your own IRC client.
* Ask a question in [stackoverflow](http://stackoverflow.com/) under the [angular-ui-bootstrap](http://stackoverflow.com/questions/tagged/angular-ui-bootstrap) tag.
* Write your question in our [mailing list](https://groups.google.com/forum/#!categories/angular-ui/bootstrap).
Project's issue on GitHub should be used discuss bugs and features.
## FAQ
https://github.com/angular-ui/bootstrap/wiki/FAQ
## Supported browsers
Directives from this repository are automatically tested with the following browsers:
* Chrome (stable and canary channel)
* Firefox
* IE 9 and 10
* Opera
* Safari
Modern mobile browsers should work without problems.
**IE 8 is not officially supported at the moment**. This project is run by volunteers and with the current number of commiters
we are not in the position to guarantee IE8 support. If you need support for IE8 we would welcome a contributor who would like to take care about IE8.
Alternatively you could sponsor this project to guarantee IE8 support.
We believe that most of the directives would work OK after:
* including relevant shims (for ES5 we recommend https://github.com/kriskowal/es5-shim)
* taking care of the steps described in http://docs.angularjs.org/guide/ie
We are simply not regularly testing against IE8.
## Project philosophy
### Native, lightweight directives
We are aiming at providing a set of AngularJS directives based on Bootstrap's markup and CSS. The goal is to provide **native AngularJS directives** without any dependency on jQuery or Bootstrap's JavaScript.
It is often better to rewrite an existing JavaScript code and create a new, pure AngularJS directive. Most of the time the resulting directive is smaller as compared to the original JavaScript code size and better integrated into the AngularJS ecosystem.
### Customizability
All the directives in this repository should have their markup externalized as templates (loaded via `templateUrl`). In practice it means that you can **customize directive's markup at will**. One could even imagine providing a non-Bootstrap version of the templates!
### Take what you need and not more
Each directive has its own AngularJS module without any dependencies on other modules or third-party JavaScript code. In practice it means that you can **just grab the code for the directives you need** and you are not obliged to drag the whole repository.
### Quality and stability
Directives should work. All the time and in all browsers. This is why all the directives have a comprehensive suite of unit tests. All the automated tests are executed on each checkin in several browsers: Chrome, ChromeCanary, Firefox, Opera, Safari, IE9.
In fact we are fortunate enough to **benefit from the same testing infrastructure as AngularJS**!
## Support
If you are having problems making some directives work, there are several ways to get help:
* Live help in the IRC (`#angularjs` channel at the `freenode` network). Use this [webchat](https://webchat.freenode.net/) or your own IRC client.
* Ask a question in [stackoverflow](http://stackoverflow.com/) under the [angular-ui-bootstrap](http://stackoverflow.com/questions/tagged/angular-ui-bootstrap) tag.
* Write your question in our [mailing list](https://groups.google.com/forum/#!categories/angular-ui/bootstrap).
Project's issue on GitHub should be used discuss bugs and features.
## Contributing to the project
We are always looking for the quality contributions! Please check the [CONTRIBUTING.md](CONTRIBUTING.md) for the contribution guidelines.
### Development
#### Prepare your environment
* Install [Node.js](http://nodejs.org/) and NPM (should come with)
* Install global dev dependencies: `npm install -g grunt-cli karma`
* Install local dev dependencies: `npm install` while current directory is bootstrap repo
#### Build
* Build the whole project: `grunt` - this will run `lint`, `test`, and `concat` targets
* To build modules, first run `grunt html2js` then `grunt build:module1:module2...:moduleN`
You can generate a custom build, containing only needed modules, from the project's homepage.
Alternatively you can run local Grunt build from the command line and list needed modules as shown below:
```javascript
grunt build:modal:tabs:alert:popover:dropdownToggle:buttons:progressbar
```
Check the Grunt build file for other tasks that are defined for this project.
#### TDD
* Run test: `grunt watch`
This will start Karma server and will continuously watch files in the project, executing tests upon every change.
#### Test coverage
Add the `--coverage` option (e.g. `grunt test --coverage`, `grunt watch --coverage`) to see reports on the test coverage. These coverage reports are found in the coverage folder.
### Customize templates
As mentioned directives from this repository have all the markup externalized in templates. You might want to customize default
templates to match your desired look & feel, add new functionality etc.
The easiest way to override an individual template is to use the `<script>` directive:
```html
<script id="template/alert/alert.html" type="text/ng-template">
<div class='alert' ng-class='type && "alert-" + type'>
<button ng-show='closeable' type='button' class='close' ng-click='close()'>Close</button>
<div ng-transclude></div>
</div>
</script>
```
If you want to override more templates it makes sense to store them as individual files and feed the `$templateCache` from those partials.
For people using Grunt as the build tool it can be easily done using the `grunt-html2js` plugin. You can also configure your own template url.
Let's have a look:
Your own template url is `views/partials/ui-bootstrap-tpls/alert/alert.html`.
Add "html2js" task to your Gruntfile
```javascript
html2js: {
options: {
base: '.',
module: 'ui-templates',
rename: function (modulePath) {
var moduleName = modulePath.replace('app/views/partials/ui-bootstrap-tpls/', '');
return 'template/' + moduleName;
}
},
main: {
src: ['app/views/partials/ui-bootstrap-tpls/**/*.html'],
dest: '.tmp/ui-templates.js'
}
}
```
Make sure to load your template.js file
`<script src="/ui-templates.js"></script>`
Inject the `ui-templates` module in your `app.js`
```javascript
angular.module('myApp', [
'ui.bootstrap',
'ui-templates'
]);
```
Then it will work fine!
For more information visit: https://github.com/karlgoldstein/grunt-html2js
### Release
* Bump up version number in `package.json`
* Commit the version change with the following message: `chore(release): [version number]`
* tag
* push changes and a tag (`git push --tags`)
* switch to the `gh-pages` branch: `git checkout gh-pages`
* copy content of the dist folder to the main folder
* Commit the version change with the following message: `chore(release): [version number]`
* push changes
* switch back to the `main branch` and modify `package.json` to bump up version for the next iteration
* commit (`chore(release): starting [version number]`) and push
* publish Bower and NuGet packages
Well done! (If you don't like repeating yourself open a PR with a grunt task taking care of the above!)

View file

@ -0,0 +1,90 @@
## Roadmap
#### Directive Maintainers
Who will take the lead regarding any pull requests or decisions for a a directive?
<table width="100%">
<th>Component</th><th>Maintainer</th>
<tr>
<td>accordion</td><td>@ajoslin</td>
</tr>
<tr>
<td>alert</td><td>@pkozlowski</td>
</tr>
<tr>
<td>bindHtml</td><td>frozen, use $sce?</td>
</tr>
<tr>
<td>buttons</td><td> @pkozlowski</td>
</tr>
<tr>
<td>carousel</td><td>@ajoslin</td>
</tr>
<tr>
<td>collapse</td><td>$animate (@chrisirhc)</td>
</tr>
<tr>
<td>datepicker</td><td>@bekos</td>
</tr>
<tr>
<td>dropdownToggle</td><td>@bekos</td>
</tr>
<tr>
<td>modal</td><td>@pkozlowski</td>
</tr>
<tr>
<td>pagination</td><td>@bekos</td>
</tr>
<tr>
<td>popover/tooltip</td><td>@chrisirhc</td>
</tr>
<tr>
<td>position</td><td>@ajoslin</td>
</tr>
<tr>
<td>progressbar</td><td>@bekos</td>
</tr>
<tr>
<td>rating</td><td>@bekos</td>
</tr>
<tr>
<td>tabs</td><td>@ajoslin</td>
</tr>
<tr>
<td>timepicker</td><td>@bekos</td>
</tr>
<tr>
<td>transition</td><td>@frozen, remove (@chrisirhc)</td>
</tr>
<tr>
<td>typeahead</td><td>@pkozlowski, @chrisirhc</td>
</tr>
</table>
#### Attribute Prefix
Each directive should make its own two-letter prefix
`<tab tb-active=”true” tb-select=”doThis()”>`
#### Use $animate
* @chrisirhc is leading this
#### New Build system
* @ajoslin is leading this
* Building everything on travis commit
* Push to bower, nuget, cdnjs, etc
#### Switch to ngdocs
* http://github.com/petebacondarwin/angular-doc-gen
### Conventions for whether attributes/options should be watched/evaluated-once
- Boolean attributes
- Stick AngularJS conventions rather than Bootstrap conventions

View file

@ -0,0 +1,21 @@
.bs-docs-social {
margin-top: 1em;
padding: 15px 0;
text-align: center;
background-color: rgba(245,245,245,0.3);
border-top: 1px solid rgba(255,255,255,0.3);
border-bottom: 1px solid rgba(221,221,221,0.3);
}
.bs-docs-social-buttons {
position: absolute;
top: 8px;
margin-left: 0;
margin-bottom: 0;
padding-left: 0;
list-style: none;
}
.bs-docs-social-buttons li {
list-style: none;
display: inline-block;
line-height: 1;
}

View file

@ -0,0 +1,13 @@
<div class="btn-toolbar btn-group pull-right">
<a class="btn btn-small btn-primary" href="https://github.com/angular-ui/bootstrap/tree/gh-pages">
<i class="icon-download-alt icon-white"></i> Download <small>(<%= pkg.version%>)</small>
</a>
</div>
<div class="margin: 10px;"></div>
<ul class="nav pull-right bs-docs-social-buttons">
<li>
<iframe src="http://ghbtns.com/github-btn.html?user=angular-ui&repo=bootstrap&type=watch&count=true"
allowtransparency="true" frameborder="0" scrolling="0" width="110" height="20"></iframe>
</li>
</ul>

View file

@ -0,0 +1,58 @@
// base path, that will be used to resolve files and exclude
basePath = '.';
// list of files / patterns to load in the browser
files = [
JASMINE,
JASMINE_ADAPTER,
'misc/test-lib/jquery-1.8.2.min.js',
'misc/test-lib/angular.js',
'misc/test-lib/angular-mocks.js',
'misc/test-lib/helpers.js',
'src/**/*.js',
'template/**/*.js'
];
// list of files to exclude
exclude = [
'src/**/docs/*'
];
// Start these browsers, currently available:
// - Chrome
// - ChromeCanary
// - Firefox
// - Opera
// - Safari
// - PhantomJS
browsers = [
'Chrome'
];
// test results reporter to use
// possible values: dots || progress
reporters = ['progress'];
reportSlowerThan = 100;
// web server port
port = 9018;
// cli runner port
runnerPort = 9100;
// enable / disable colors in the output (reporters and logs)
colors = true;
// level of logging
// possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG
logLevel = LOG_INFO;
// enable / disable watching file and executing tests whenever any file changes
autoWatch = false;
// Continuous Integration mode
// if true, it capture browsers, run tests and exit
singleRun = false;

View file

@ -0,0 +1,16 @@
# <%= version%> (<%= today%>)
<% if (_(changelog.feat).size() > 0) { %>
## Features
<% _(changelog.feat).keys().sort().forEach(function(scope) { %>
- **<%= scope%>:** <% changelog.feat[scope].forEach(function(change) { %>
- <%= change.msg%> (<%= helpers.commitLink(change.sha1) %>) <% }); %><% }); %> <% } %>
<% if (_(changelog.fix).size() > 0) { %>
## Bug Fixes
<% _(changelog.fix).keys().sort().forEach(function(scope) { %>
- **<%= scope%>:** <% changelog.fix[scope].forEach(function(change) { %>
- <%= change.msg%> (<%= helpers.commitLink(change.sha1) %>) <% }); %><% }); %> <% } %>
<% if (_(changelog.breaking).size() > 0) { %>
## Breaking Changes
<% _(changelog.breaking).keys().sort().forEach(function(scope) { %>
- **<%= scope%>:** <% changelog.breaking[scope].forEach(function(change) { %>
<%= change.msg%><% }); %><% }); %> <% } %>

View file

@ -0,0 +1,281 @@
/* global FastClick, smoothScroll */
angular.module('ui.bootstrap.demo', ['ui.bootstrap', 'plunker', 'ngTouch'], function($httpProvider){
FastClick.attach(document.body);
delete $httpProvider.defaults.headers.common['X-Requested-With'];
}).run(['$location', function($location){
//Allows us to navigate to the correct element on initialization
if ($location.path() !== '' && $location.path() !== '/') {
smoothScroll(document.getElementById($location.path().substring(1)), 500, function(el) {
location.replace('#' + el.id);
});
}
}]).factory('buildFilesService', function ($http, $q) {
var moduleMap;
var rawFiles;
return {
getModuleMap: getModuleMap,
getRawFiles: getRawFiles,
get: function () {
return $q.all({
moduleMap: getModuleMap(),
rawFiles: getRawFiles(),
});
}
};
function getModuleMap() {
return moduleMap ? $q.when(moduleMap) : $http.get('assets/module-mapping.json')
.then(function (result) {
moduleMap = result.data;
return moduleMap;
});
}
function getRawFiles() {
return rawFiles ? $q.when(rawFiles) : $http.get('assets/raw-files.json')
.then(function (result) {
rawFiles = result.data;
return rawFiles;
});
}
});
var builderUrl = "http://50.116.42.77:3001";
function MainCtrl($scope, $http, $document, $modal, orderByFilter) {
$scope.showBuildModal = function() {
var modalInstance = $modal.open({
templateUrl: 'buildModal.html',
controller: 'SelectModulesCtrl',
resolve: {
modules: function(buildFilesService) {
return buildFilesService.getModuleMap()
.then(function (moduleMap) {
return Object.keys(moduleMap);
});
}
}
});
};
$scope.showDownloadModal = function() {
var modalInstance = $modal.open({
templateUrl: 'downloadModal.html',
controller: 'DownloadCtrl'
});
};
}
var SelectModulesCtrl = function($scope, $modalInstance, modules, buildFilesService) {
$scope.selectedModules = [];
$scope.modules = modules;
$scope.selectedChanged = function(module, selected) {
if (selected) {
$scope.selectedModules.push(module);
} else {
$scope.selectedModules.splice($scope.selectedModules.indexOf(module), 1);
}
};
$scope.downloadBuild = function () {
$modalInstance.close($scope.selectedModules);
};
$scope.cancel = function () {
$modalInstance.dismiss();
};
$scope.isOldBrowser = function () {
return isOldBrowser;
};
$scope.build = function (selectedModules, version) {
/* global JSZip, saveAs */
var moduleMap, rawFiles;
buildFilesService.get().then(function (buildFiles) {
moduleMap = buildFiles.moduleMap;
rawFiles = buildFiles.rawFiles;
generateBuild();
});
function generateBuild() {
var srcModuleNames = selectedModules
.map(function (module) {
return moduleMap[module];
})
.reduce(function (toBuild, module) {
addIfNotExists(toBuild, module.name);
module.dependencies.forEach(function (depName) {
addIfNotExists(toBuild, depName);
});
return toBuild;
}, []);
var srcModules = srcModuleNames
.map(function (moduleName) {
return moduleMap[moduleName];
});
var srcModuleFullNames = srcModules
.map(function (module) {
return module.moduleName;
});
var srcJsContent = srcModules
.reduce(function (buildFiles, module) {
return buildFiles.concat(module.srcFiles);
}, [])
.map(getFileContent)
.join('\n')
;
var jsFile = createNoTplFile(srcModuleFullNames, srcJsContent);
var tplModuleNames = srcModules
.reduce(function (tplModuleNames, module) {
return tplModuleNames.concat(module.tplModules);
}, []);
var tplJsContent = srcModules
.reduce(function (buildFiles, module) {
return buildFiles.concat(module.tpljsFiles);
}, [])
.map(getFileContent)
.join('\n')
;
var jsTplFile = createWithTplFile(srcModuleFullNames, srcJsContent, tplModuleNames, tplJsContent);
var zip = new JSZip();
zip.file('ui-bootstrap-custom-' + version + '.js', rawFiles.banner + jsFile);
zip.file('ui-bootstrap-custom-' + version + '.min.js', rawFiles.banner + uglify(jsFile));
zip.file('ui-bootstrap-custom-tpls-' + version + '.js', rawFiles.banner + jsTplFile);
zip.file('ui-bootstrap-custom-tpls-' + version + '.min.js', rawFiles.banner + uglify(jsTplFile));
saveAs(zip.generate({type: 'blob'}), 'ui-bootstrap-custom-build.zip');
}
function createNoTplFile(srcModuleNames, srcJsContent) {
return 'angular.module("ui.bootstrap", [' + srcModuleNames.join(',') + ']);\n' +
srcJsContent;
}
function createWithTplFile(srcModuleNames, srcJsContent, tplModuleNames, tplJsContent) {
var depModuleNames = srcModuleNames.slice();
depModuleNames.unshift('"ui.bootstrap.tpls"');
return 'angular.module("ui.bootstrap", [' + depModuleNames.join(',') + ']);\n' +
'angular.module("ui.bootstrap.tpls", [' + tplModuleNames.join(',') + ']);\n' +
srcJsContent + '\n' + tplJsContent;
}
function addIfNotExists(array, element) {
if (array.indexOf(element) == -1) {
array.push(element);
}
}
function getFileContent(fileName) {
return rawFiles.files[fileName];
}
function uglify(js) {
/* global UglifyJS */
var ast = UglifyJS.parse(js);
ast.figure_out_scope();
var compressor = UglifyJS.Compressor();
var compressedAst = ast.transform(compressor);
compressedAst.figure_out_scope();
compressedAst.compute_char_frequency();
compressedAst.mangle_names();
var stream = UglifyJS.OutputStream();
compressedAst.print(stream);
return stream.toString();
}
};
};
var DownloadCtrl = function($scope, $modalInstance) {
$scope.options = {
minified: true,
tpls: true
};
$scope.download = function (version) {
var options = $scope.options;
var downloadUrl = ['ui-bootstrap-'];
if (options.tpls) {
downloadUrl.push('tpls-');
}
downloadUrl.push(version);
if (options.minified) {
downloadUrl.push('.min');
}
downloadUrl.push('.js');
return downloadUrl.join('');
};
$scope.cancel = function () {
$modalInstance.dismiss();
};
};
/*
* The following compatibility check is from:
*
* Bootstrap Customizer (http://getbootstrap.com/customize/)
* Copyright 2011-2014 Twitter, Inc.
*
* Licensed under the Creative Commons Attribution 3.0 Unported License. For
* details, see http://creativecommons.org/licenses/by/3.0/.
*/
var isOldBrowser;
(function () {
var supportsFile = (window.File && window.FileReader && window.FileList && window.Blob);
function failback() {
isOldBrowser = true;
}
/**
* Based on:
* Blob Feature Check v1.1.0
* https://github.com/ssorallen/blob-feature-check/
* License: Public domain (http://unlicense.org)
*/
var url = window.webkitURL || window.URL; // Safari 6 uses "webkitURL".
var svg = new Blob(
['<svg xmlns=\'http://www.w3.org/2000/svg\'></svg>'],
{ type: 'image/svg+xml;charset=utf-8' }
);
var objectUrl = url.createObjectURL(svg);
if (/^blob:/.exec(objectUrl) === null || !supportsFile) {
// `URL.createObjectURL` created a URL that started with something other
// than "blob:", which means it has been polyfilled and is not supported by
// this browser.
failback();
} else {
angular.element('<img/>')
.on('load', function () {
isOldBrowser = false;
})
.on('error', failback)
.attr('src', objectUrl);
}
})();

View file

@ -0,0 +1,323 @@
body {
opacity: 1;
-webkit-transition: opacity 1s ease;
-moz-transition: opacity 1s ease;
transition: opacity 1s;
}
.ng-cloak {
opacity: 0;
}
.ng-invalid {
border: 1px solid red !important;
}
section {
padding-top: 30px;
}
.page-header h1 > small > a {
color: #999;
}
.page-header h1 > small > a:hover {
text-decoration: none;
}
.footer {
text-align: center;
padding: 30px 0;
margin-top: 70px;
border-top: 1px solid #e5e5e5;
background-color: #f5f5f5;
}
.bs-social {
margin-top: 20px;
margin-bottom: 20px;
text-align: center;
}
@media (min-width: 768px) {
.bs-social {
text-align: left;
}
}
.nav, .pagination, .carousel, .panel-title a {
cursor: pointer;
}
.bs-social-buttons {
display: inline-block;
margin-bottom: 0;
padding-left: 0;
list-style: none;
}
.bs-social-buttons li {
display: inline-block;
padding: 5px 8px;
line-height: 1;
}
@media (max-width: 767px) {
.visible-xs.collapse.in {
display: block!important;
}
.visible-xs.collapse {
display: none!important;
}
}
.navbar .collapse {
border-top: 1px solid #e7e7e7;
margin-left: -15px;
margin-right: -15px;
padding-right: 15px;
padding-left: 15px;
}
.show-grid {
margin-bottom: 15px;
}
/*
* Container
*
* Tweak to width of container.
*/
/*@media (min-width: 1200px) {
.container{
max-width: 970px;
}
}*/
/*
* Tabs
*
* Tweaks to the Tabs.
*/
.code .nav-tabs {
border-bottom: 1px solid #ccc;
}
.code pre, .code code {
border-top: none;
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.code .nav-tabs>li.active>a, .code .nav-tabs>li.active>a:hover, .code .nav-tabs>li.active>a:focus {
background-color: #f8f8f8;
border: 1px solid #ccc;
border-bottom-color: transparent;
}
/*
* Button Inverse
*
* Buttons in the masthead.
*/
.btn-outline-inverse {
color: #fff;
background-color: transparent;
border-color: #cdbfe3;
margin: 10px;
}
@media (min-width: 768px) {
.btn-outline-inverse {
width: auto;
margin: 20px 5px 20px 0;
padding: 18px 24px;
font-size: 21px;
}
}
.btn-outline-inverse:hover, .btn-outline-inverse:focus, .btn-outline-inverse:active {
color: #563d7c;
text-shadow: none;
background-color: #fff;
border-color: #fff;
}
/* Page headers */
.bs-header {
padding: 30px 15px 40px; /* side padding builds on .container 15px, so 30px */
font-size: 16px;
text-align: center;
text-shadow: 0 1px 0 rgba(0,0,0,.15);
color: #cdbfe3;
background-color: #563d7c;
background-image: url(header.png);
}
.bs-header a {
color: #fff;
font-weight: normal;
}
.bs-header h1 {
color: #fff;
}
.bs-header p {
font-weight: 200;
line-height: 1.4;
}
.bs-header .container {
position: relative;
}
@media (min-width: 768px) {
.bs-header {
font-size: 30px;
text-align: left;
}
.bs-header h1 {
font-size: 100px;
line-height: 1;
}
}
@media (min-width: 992px) {
.bs-header p {
margin-right: 25%;
}
}
.navbar-inner {
-webkit-box-shadow: 0 3px 3px rgba(0,0,0,0.175);
box-shadow: 0 3px 3px rgba(0,0,0,0.175);
}
/*
* Side navigation
*
* Scrollspy and affixed enhanced navigation to highlight sections and secondary
* sections of docs content.
*/
/* By default it's not affixed in mobile views, so undo that */
.bs-sidebar.affix {
position: static;
}
/* First level of nav */
.bs-sidenav {
margin-top: 30px;
margin-bottom: 30px;
padding-top: 10px;
padding-bottom: 10px;
text-shadow: 0 1px 0 #fff;
background-color: #f7f5fa;
border-radius: 5px;
}
/* All levels of nav */
.bs-sidebar .nav > li > a {
display: block;
color: #716b7a;
padding: 5px 20px;
}
.bs-sidebar .nav > li > a:hover,
.bs-sidebar .nav > li > a:focus {
text-decoration: none;
background-color: #e5e3e9;
border-right: 1px solid #dbd8e0;
}
.bs-sidebar .nav > .active > a,
.bs-sidebar .nav > .active:hover > a,
.bs-sidebar .nav > .active:focus > a {
font-weight: bold;
color: #563d7c;
background-color: transparent;
border-right: 1px solid #563d7c;
}
/* Nav: second level (shown on .active) */
.bs-sidebar .nav .nav {
display: none; /* Hide by default, but at >768px, show it */
margin-bottom: 8px;
}
.bs-sidebar .nav .nav > li > a {
padding-top: 3px;
padding-bottom: 3px;
padding-left: 30px;
font-size: 90%;
}
/* Show and affix the side nav when space allows it */
@media (min-width: 992px) {
.bs-sidebar .nav > .active > ul {
display: block;
}
/* Widen the fixed sidebar */
.bs-sidebar.affix,
.bs-sidebar.affix-bottom {
width: 213px;
}
.bs-sidebar.affix {
position: fixed; /* Undo the static from mobile first approach */
top: 80px;
}
.bs-sidebar.affix-bottom {
position: absolute; /* Undo the static from mobile first approach */
}
.bs-sidebar.affix-bottom .bs-sidenav,
.bs-sidebar.affix .bs-sidenav {
margin-top: 0;
margin-bottom: 0;
}
}
@media (min-width: 1200px) {
/* Widen the fixed sidebar again */
.bs-sidebar.affix-bottom,
.bs-sidebar.affix {
width: 263px;
}
}
/* Not enough room on mobile for markup tab, js tab, and plunk btn.
And no one cares about plunk button on a phone anyway */
@media only screen and (max-device-width: 480px) {
#plunk-btn {
display: none;
}
}
.navbar-nav .dropdown .navbar-brand {
max-width: 100%;
margin-right: inherit;
margin-left: inherit;
}
.header-placeholder {
height: 50px;
}
@media screen and (min-width: 768px) {
.dropdown.open > .navbar-brand + .dropdown-menu {
left: 10px;
}
.header-placeholder {
height: 50px;
}
.navbar-nav .dropdown .navbar-brand {
max-width: 200px;
margin-right: 5px;
margin-left: 10px;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 398 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -0,0 +1,58 @@
angular.module('plunker', [])
.factory('plunkGenerator', function ($document) {
return function (ngVersion, bsVersion, version, module, content) {
var form = angular.element('<form style="display: none;" method="post" action="http://plnkr.co/edit/?p=preview" target="_blank"></form>');
var addField = function (name, value) {
var input = angular.element('<input type="hidden" name="' + name + '">');
input.attr('value', value);
form.append(input);
};
var indexContent = function (content, version) {
return '<!doctype html>\n' +
'<html ng-app="ui.bootstrap.demo">\n' +
' <head>\n' +
' <script src="//ajax.googleapis.com/ajax/libs/angularjs/'+ngVersion+'/angular.js"></script>\n' +
' <script src="//angular-ui.github.io/bootstrap/ui-bootstrap-tpls-'+version+'.js"></script>\n' +
' <script src="example.js"></script>\n' +
' <link href="//netdna.bootstrapcdn.com/bootstrap/'+bsVersion+'/css/bootstrap.min.css" rel="stylesheet">\n' +
' </head>\n' +
' <body>\n\n' +
content + '\n' +
' </body>\n' +
'</html>\n';
};
var scriptContent = function(content) {
return "angular.module('ui.bootstrap.demo', ['ui.bootstrap']);" + "\n" + content;
};
addField('description', 'http://angular-ui.github.io/bootstrap/');
addField('files[index.html]', indexContent(content.markup, version));
addField('files[example.js]', scriptContent(content.javascript));
$document.find('body').append(form);
form[0].submit();
form.remove();
};
})
.controller('PlunkerCtrl', function ($scope, plunkGenerator) {
$scope.content = {};
$scope.edit = function (ngVersion, bsVersion, version, module) {
plunkGenerator(ngVersion, bsVersion, version, module, $scope.content);
};
})
.directive('plunkerContent', function () {
return {
link:function (scope, element, attrs) {
scope.content[attrs.plunkerContent] = element.text().trim();
}
}
});

View file

@ -0,0 +1,59 @@
/**
* Generic language patterns
*
* @author Craig Campbell
* @version 1.0.9
*/
Rainbow.extend([
{
'matches': {
1: {
'name': 'keyword.operator',
'pattern': /\=/g
},
2: {
'name': 'string',
'matches': {
'name': 'constant.character.escape',
'pattern': /\\('|"){1}/g
}
}
},
'pattern': /(\(|\s|\[|\=|:)(('|")([^\\\1]|\\.)*?(\3))/gm
},
{
'name': 'comment',
'pattern': /\/\*[\s\S]*?\*\/|(\/\/|\#)[\s\S]*?$/gm
},
{
'name': 'constant.numeric',
'pattern': /\b(\d+(\.\d+)?(e(\+|\-)?\d+)?(f|d)?|0x[\da-f]+)\b/gi
},
{
'matches': {
1: 'keyword'
},
'pattern': /\b(and|array|as|bool(ean)?|c(atch|har|lass|onst)|d(ef|elete|o(uble)?)|e(cho|lse(if)?|xit|xtends|xcept)|f(inally|loat|or(each)?|unction)|global|if|import|int(eger)?|long|new|object|or|pr(int|ivate|otected)|public|return|self|st(ring|ruct|atic)|switch|th(en|is|row)|try|(un)?signed|var|void|while)(?=\(|\b)/gi
},
{
'name': 'constant.language',
'pattern': /true|false|null/g
},
{
'name': 'keyword.operator',
'pattern': /\+|\!|\-|&(gt|lt|amp);|\||\*|\=/g
},
{
'matches': {
1: 'function.call'
},
'pattern': /(\w+?)(?=\()/g
},
{
'matches': {
1: 'storage.function',
2: 'entity.name.function'
},
'pattern': /(function)\s(.*?)(?=\()/g
}
]);

View file

@ -0,0 +1,83 @@
/**
* HTML patterns
*
* @author Craig Campbell
* @version 1.0.7
*/
Rainbow.extend('html', [
{
'name': 'source.php.embedded',
'matches': {
2: {
'language': 'php'
}
},
'pattern': /&lt;\?=?(?!xml)(php)?([\s\S]*?)(\?&gt;)/gm
},
{
'name': 'source.css.embedded',
'matches': {
0: {
'language': 'css'
}
},
'pattern': /&lt;style(.*?)&gt;([\s\S]*?)&lt;\/style&gt;/gm
},
{
'name': 'source.js.embedded',
'matches': {
0: {
'language': 'javascript'
}
},
'pattern': /&lt;script(?! src)(.*?)&gt;([\s\S]*?)&lt;\/script&gt;/gm
},
{
'name': 'comment.html',
'pattern': /&lt;\!--[\S\s]*?--&gt;/g
},
{
'matches': {
1: 'support.tag.open',
2: 'support.tag.cclose'
},
'pattern': /(&lt;)|(\/?\??&gt;)/g
},
{
'name': 'support.tag',
'matches': {
1: 'support.tag',
2: 'support.tag.special',
3: 'support.tag-name'
},
'pattern': /(&lt;\??)(\/|\!?)(\w+)/g
},
{
'matches': {
1: 'support.attribute'
},
'pattern': /([a-z-]+)(?=\=)/gi
},
{
'matches': {
1: 'support.operator',
2: 'string.quote',
3: 'string.value',
4: 'string.quote'
},
'pattern': /(=)('|")(.*?)(\2)/g
},
{
'matches': {
1: 'support.operator',
2: 'support.value'
},
'pattern': /(=)([a-zA-Z\-0-9]*)\b/g
},
{
'matches': {
1: 'support.attribute'
},
'pattern': /\s(\w+)(?=\s|&gt;)(?![\s\S]*&lt;)/g
}
], true);

View file

@ -0,0 +1,110 @@
/**
* Javascript patterns
*
* @author Craig Campbell
* @version 1.0.7
*/
Rainbow.extend('javascript', [
/**
* matches $. or $(
*/
{
'name': 'selector',
'pattern': /(\s|^)\$(?=\.|\()/g
},
{
'name': 'support',
'pattern': /\b(window|document)\b/g
},
{
'matches': {
1: 'support.property'
},
'pattern': /\.(length|node(Name|Value))\b/g
},
{
'matches': {
1: 'support.function'
},
'pattern': /(setTimeout|setInterval)(?=\()/g
},
{
'matches': {
1: 'support.method'
},
'pattern': /\.(getAttribute|push|getElementById|getElementsByClassName|log|setTimeout|setInterval)(?=\()/g
},
{
'matches': {
1: 'support.tag.script',
2: [
{
'name': 'string',
'pattern': /('|")(.*?)(\1)/g
},
{
'name': 'entity.tag.script',
'pattern': /(\w+)/g
}
],
3: 'support.tag.script'
},
'pattern': /(&lt;\/?)(script.*?)(&gt;)/g
},
/**
* matches any escaped characters inside of a js regex pattern
*
* @see https://github.com/ccampbell/rainbow/issues/22
*
* this was causing single line comments to fail so it now makes sure
* the opening / is not directly followed by a *
*
* @todo check that there is valid regex in match group 1
*/
{
'name': 'string.regexp',
'matches': {
1: 'string.regexp.open',
2: {
'name': 'constant.regexp.escape',
'pattern': /\\(.){1}/g
},
3: 'string.regexp.cclose',
4: 'string.regexp.modifier'
},
'pattern': /(\/)(?!\*)(.+)(\/)([igm]{0,3})/g
},
/**
* matches runtime function declarations
*/
{
'matches': {
1: 'storage',
3: 'entity.function'
},
'pattern': /(var)?(\s|^)(.*)(?=\s?=\s?function\()/g
},
/**
* matches constructor call
*/
{
'matches': {
1: 'keyword',
2: 'entity.function'
},
'pattern': /(new)\s+(.*)(?=\()/g
},
/**
* matches any function call in the style functionName: function()
*/
{
'name': 'entity.function',
'pattern': /(\w+)(?=:\s{0,}function)/g
}
]);

View file

@ -0,0 +1,88 @@
/**
* GitHub theme
*
* @author Craig Campbell
* @version 1.0.4
*/
pre {
border: 1px solid #ccc;
word-wrap: break-word;
padding: 6px 10px;
line-height: 19px;
margin-bottom: 20px;
}
code {
border: 1px solid #eaeaea;
margin: 0 2px;
padding: 0 5px;
font-size: 12px;
}
pre code {
border: 0;
padding: 0;
margin: 0;
-moz-border-radius: 0;
-webkit-border-radius: 0;
border-radius: 0;
}
pre, code {
font-family: Consolas, 'Liberation Mono', Courier, monospace;
color: #333;
background: #f8f8f8;
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
border-radius: 3px;
}
pre, pre code {
font-size: 13px;
}
pre .comment {
color: #998;
}
pre .support {
color: #0086B3;
}
pre .tag, pre .tag-name {
color: navy;
}
pre .keyword, pre .css-property, pre .vendor-prefix, pre .sass, pre .class, pre .id, pre .css-value, pre .entity.function, pre .storage.function {
font-weight: bold;
}
pre .css-property, pre .css-value, pre .vendor-prefix, pre .support.namespace {
color: #333;
}
pre .constant.numeric, pre .keyword.unit, pre .hex-color {
font-weight: normal;
color: #099;
}
pre .entity.class {
color: #458;
}
pre .entity.id, pre .entity.function {
color: #900;
}
pre .attribute, pre .variable {
color: teal;
}
pre .string, pre .support.value {
font-weight: normal;
color: #d14;
}
pre .regexp {
color: #009926;
}

View file

@ -0,0 +1,773 @@
/**
* Copyright 2012 Craig Campbell
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Rainbow is a simple code syntax highlighter
*
* @preserve @version 1.1.8
* @url rainbowco.de
*/
window['Rainbow'] = (function() {
/**
* array of replacements to process at the end
*
* @type {Object}
*/
var replacements = {},
/**
* an array of start and end positions of blocks to be replaced
*
* @type {Object}
*/
replacement_positions = {},
/**
* an array of the language patterns specified for each language
*
* @type {Object}
*/
language_patterns = {},
/**
* an array of languages and whether they should bypass the default patterns
*
* @type {Object}
*/
bypass_defaults = {},
/**
* processing level
*
* replacements are stored at this level so if there is a sub block of code
* (for example php inside of html) it runs at a different level
*
* @type {number}
*/
CURRENT_LEVEL = 0,
/**
* constant used to refer to the default language
*
* @type {number}
*/
DEFAULT_LANGUAGE = 0,
/**
* used as counters so we can selectively call setTimeout
* after processing a certain number of matches/replacements
*
* @type {number}
*/
match_counter = 0,
/**
* @type {number}
*/
replacement_counter = 0,
/**
* @type {null|string}
*/
global_class,
/**
* @type {null|Function}
*/
onHighlight;
/**
* cross browser get attribute for an element
*
* @see http://stackoverflow.com/questions/3755227/cross-browser-javascript-getattribute-method
*
* @param {Node} el
* @param {string} attr attribute you are trying to get
* @returns {string|number}
*/
function _attr(el, attr, attrs, i) {
var result = (el.getAttribute && el.getAttribute(attr)) || 0;
if (!result) {
attrs = el.attributes;
for (i = 0; i < attrs.length; ++i) {
if (attrs[i].nodeName === attr) {
return attrs[i].nodeValue;
}
}
}
return result;
}
/**
* adds a class to a given code block
*
* @param {Element} el
* @param {string} class_name class name to add
* @returns void
*/
function _addClass(el, class_name) {
el.className += el.className ? ' ' + class_name : class_name;
}
/**
* checks if a block has a given class
*
* @param {Element} el
* @param {string} class_name class name to check for
* @returns {boolean}
*/
function _hasClass(el, class_name) {
return (' ' + el.className + ' ').indexOf(' ' + class_name + ' ') > -1;
}
/**
* gets the language for this block of code
*
* @param {Element} block
* @returns {string|null}
*/
function _getLanguageForBlock(block) {
// if this doesn't have a language but the parent does then use that
// this means if for example you have: <pre data-language="php">
// with a bunch of <code> 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: <pre class="lang-php">
// or the HTML5 style: <pre><code class="language-php">
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, '&lt;').replace(/>/g, '&gt;').replace(/&(?![\w\#]+;)/g, '&amp;');
}
/**
* 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 '<span class="' + name.replace(/\./g, ' ') + (global_class ? ' ' + global_class : '') + '">' + code + '</span>';
}
/**
* 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 100 items we process let's call set timeout
// to let the ui breathe a little
return match_counter % 100 > 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 <code> 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;

View file

@ -0,0 +1,97 @@
/*
* 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 diff suppressed because one or more lines are too long

View file

@ -0,0 +1,331 @@
<!DOCTYPE html>
<html lang="en" ng-app="ui.bootstrap.demo" id="top">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Angular directives for Bootstrap</title>
<meta name="description" content="AngularJS (Angular) native directives for Bootstrap. Small footprint (5kB gzipped!), no 3rd party JS dependencies (jQuery, bootstrap JS) required! Widgets: <% demoModules.forEach(function(module) { %><%= module.displayName %>, <% }); %>">
<meta name="google-site-verification" content="7lc5HyceLDqpV_6oNHteYFfxDJH7-S3DwnJKtNUKcRg" />
<script src="//cdnjs.cloudflare.com/ajax/libs/fastclick/0.6.7/fastclick.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/FileSaver.js/1.0.0/FileSaver.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/jszip/2.4.0/jszip.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/<%= ngversion %>/angular.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/<%= ngversion %>/angular-touch.min.js"></script>
<script src="ui-bootstrap-tpls-<%= pkg.version%>.min.js"></script>
<script src="assets/plunker.js"></script>
<script src="assets/app.js"></script>
<link href="//netdna.bootstrapcdn.com/bootstrap/<%= bsversion %>/css/bootstrap.min.css" rel="stylesheet"/>
<link rel="stylesheet" href="assets/rainbow.css"/>
<link rel="stylesheet" href="assets/demo.css"/>
<link rel="author" href="https://github.com/angular-ui/bootstrap/graphs/contributors">
</head>
<body class="ng-cloak" ng-controller="MainCtrl">
<header class="navbar navbar-default navbar-fixed-top">
<div class="navbar-inner">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#bs-example-navbar-collapse-3" ng-click="isCollapsed = !isCollapsed">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand visible-xs" href="#">UI Bootstrap</a>
</div>
<nav class="hidden-xs">
<ul class="nav navbar-nav">
<a href="#top" role="button" class="navbar-brand">
UI Bootstrap
</a>
<li class="dropdown" dropdown>
<a role="button" class="dropdown-toggle" dropdown-toggle>
Directives <b class="caret"></b>
</a>
<ul class="dropdown-menu">
<% demoModules.forEach(function(module) { %><li><a href="#<%= module.name %>"><%= module.displayName %></a></li><% }); %>
</ul>
</li>
<li><a href="#getting_started">Getting started</a></li>
</ul>
</nav>
<nav class="visible-xs" collapse="!isCollapsed">
<ul class="nav navbar-nav">
<li><a href="#getting_started" ng-click="isCollapsed = !isCollapsed">Getting started</a></li>
<li><a href="#directives_small" ng-click="isCollapsed = !isCollapsed">Directives</a></li>
</ul>
</nav>
</div>
</div>
</header>
<div class="header-placeholder"></div>
<div role="main">
<header class="bs-header text-center" id="overview">
<div class="container">
<h1>
UI Bootstrap
</h1>
<p>Bootstrap components written in pure <a href="http://angularjs.org">AngularJS</a> by the <a
href="http://angular-ui.github.io">AngularUI Team</a></p>
<p>
<a class="btn btn-outline-inverse btn-lg" href="https://github.com/angular-ui/bootstrap"><i class="icon-github"></i>Code on Github</a>
<button type="button" class="btn btn-outline-inverse btn-lg" ng-click="showDownloadModal()">
<i class="glyphicon glyphicon-download-alt"></i> Download <small>(<%= pkg.version%>)</small>
</button>
<button type="button" class="btn btn-outline-inverse btn-lg" ng-click="showBuildModal()"><i class="glyphicon glyphicon-wrench"></i> Create a Build</button>
</p>
</div>
<div class="bs-social container">
<ul class="bs-social-buttons">
<li>
<iframe src="//ghbtns.com/github-btn.html?user=angular-ui&repo=bootstrap&type=watch&count=true"
allowtransparency="true" frameborder="0" scrolling="0" width="110" height="20"></iframe>
</li>
<li>
<iframe src="//ghbtns.com/github-btn.html?user=angular-ui&repo=bootstrap&type=fork&count=true"
allowtransparency="true" frameborder="0" scrolling="0" width="110" height="20"></iframe>
</li>
<li>
<a href="https://twitter.com/share" class="twitter-share-button"
data-hashtags="angularjs">Tweet</a>
<script>!function (d, s, id) {
var js, fjs = d.getElementsByTagName(s)[0];
if (!d.getElementById(id)) {
js = d.createElement(s);
js.id = id;
js.src = "//platform.twitter.com/widgets.js";
fjs.parentNode.insertBefore(js, fjs);
}
}(document, "script", "twitter-wjs");</script>
</li>
<li>
<!-- Place this tag where you want the +1 button to render. -->
<div class="g-plusone" data-size="medium"></div>
<!-- Place this tag after the last +1 button tag. -->
<script type="text/javascript">
(function () {
var po = document.createElement('script');
po.type = 'text/javascript';
po.async = true;
po.src = 'https://apis.google.com/js/plusone.js';
var s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(po, s);
})();
</script>
</li>
</ul>
</div>
</header>
<div class="container">
<div class="row">
<div class="col-md-12">
<section class="bs-sidebar visible-xs" id="directives_small">
<ul class="nav bs-sidenav">
<li><a href="#"><strong>Directives</strong></a></li>
<% demoModules.forEach(function(module) { %>
<li><a href="#<%= module.name %>"><%= module.displayName %></a></li>
<% }); %>
</ul>
</section>
<section id="getting_started">
<div class="page-header">
<h1>Getting started</h1>
</div>
<h3>Dependencies</h3>
<p>
This repository contains a set of <strong>native AngularJS directives</strong> based on
Bootstrap's markup and CSS. As a result no dependency on jQuery or Bootstrap's
JavaScript is required. The <strong>only required dependencies</strong> are:
</p>
<ul>
<li><a href="http://angularjs.org" target="_blank">AngularJS</a> (requires AngularJS 1.2.x, tested with <%= ngversion %>)</li>
<li><a href="http://getbootstrap.com" target="_blank">Bootstrap CSS</a> (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.
</li>
</ul>
<h3>Files to download</h3>
<p>
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
<a href="https://github.com/angular-ui/bootstrap/tree/gh-pages">downloaded from here</a>.
</p>
<p>Alternativelly, if you are only interested in a subset of directives, you can
<a ng-click="showBuildModal()">create your own build</a>.
</p>
<p>Whichever method you choose the good news that the overall size of a download is very small:
&lt;20kB for all directives (~5kB with gzip compression!)</p>
<h3>Installation</h3>
<p>As soon as you've got all the files downloaded and included in your page you just need to declare
a dependency on the <code>ui.bootstrap</code> <a href="http://docs.angularjs.org/guide/module">module</a>:<br>
<pre><code>angular.module('myModule', ['ui.bootstrap']);</code></pre>
</p>
<p>You can fork one of the plunkers from this page to see a working example of what is described here.</p>
<h3>CSS</h3>
<p>Original Bootstrap's CSS depends on empty <code>href</code> attributes to style cursors for several components (pagination, tabs etc.).
But in AngularJS adding empty <code>href</code> attributes to link tags will cause unwanted route changes. This is why we need to remove empty <code>href</code> 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: <pre><code>.nav, .pagination, .carousel, .panel-title a { cursor: pointer; }</code></pre>
</p>
<h3>FAQ</h3>
<p>Please check <a href="https://github.com/angular-ui/bootstrap/wiki/FAQ" target="_blank">our FAQ section</a> for common problems / solutions.</p>
</section>
<% demoModules.forEach(function(module) { %>
<section id="<%= module.name %>">
<div class="page-header">
<h1><%= module.displayName %><small>
(<a target="_blank" href="https://github.com/angular-ui/bootstrap/tree/master/src/<%= module.name %>">ui.bootstrap.<%= module.name %></a>)
</small></h1>
</div>
<div class="row">
<div class="col-md-6 show-grid">
<%= module.docs.html %>
</div>
<div class="col-md-6">
<%= module.docs.md %>
</div>
</div>
<hr>
<div class="row code">
<div class="col-md-12" ng-controller="PlunkerCtrl">
<div class="pull-right">
<button class="btn btn-info" id="plunk-btn" ng-click="edit('<%= ngversion%>', '<%= bsversion %>', '<%= pkg.version%>', '<%= module.name %>')"><i class="glyphicon glyphicon-edit"></i> Edit in plunker</button>
</div>
<tabset>
<tab heading="Markup">
<div plunker-content="markup">
<pre ng-non-bindable><code data-language="html"><%- module.docs.html %></code></pre>
</div>
</tab>
<tab heading="JavaScript">
<div plunker-content="javascript">
<pre ng-non-bindable><code data-language="javascript"><%- module.docs.js %></code></pre>
</div>
</tab>
</tabset>
</div>
</div>
</section>
<script><%= module.docs.js %></script>
<% }); %>
</div>
</div>
</div><!-- /.container -->
</div><!-- /.main -->
<footer class="footer">
<div class="container">
<p>Designed and built by <a href="https://github.com/angular-ui?tab=members" target="_blank">Angular-UI team</a> and <a href="https://github.com/angular-ui/bootstrap/graphs/contributors" target="_blank">contributors</a>.</p>
<p>Code licensed under <a href="https://github.com/angular-ui/bootstrap/blob/master/LICENSE"><%= pkg.license %> License</a>.</p>
<p><a href="https://github.com/angular-ui/bootstrap/issues?state=open">Issues</a></p>
</div>
</footer>
<script src="assets/rainbow.js"></script>
<script src="assets/rainbow-generic.js"></script>
<script src="assets/rainbow-javascript.js"></script>
<script src="assets/rainbow-html.js"></script>
<script type="text/ng-template" id="downloadModal.html">
<div class="modal-header"><h4 class="modal-title">Download Angular UI Bootstrap</h4></div>
<div class="modal-body">
<form class="form-horizontal">
<div class="form-group">
<label class="col-sm-3 control-label"><strong>Build</strong></label>
<div class="col-sm-9">
<span class="btn-group">
<button type="button" class="btn btn-default" ng-model="options.minified" btn-radio="true">Minified</button>
<button type="button" class="btn btn-default" ng-model="options.minified" btn-radio="false">Uncompressed</button>
</span>
<small class="help-block">Use <b>Minified</b> version in your deployed application. <b>Uncompressed</b> source code is useful only for debugging and development purpose.</small>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label"><strong>Include Templates</strong></label>
<div class="col-sm-9">
<span class="btn-group">
<button type="button" class="btn btn-default" ng-model="options.tpls" btn-radio="true">Yes</button>
<button type="button" class="btn btn-default" ng-model="options.tpls" btn-radio="false">No</button>
</span>
<small class="help-block">Whether you want to include the <i>default templates</i>, bundled with most of the directives. These templates are based on Bootstrap's markup and CSS.</small>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label"><strong>Bower</strong></label>
<div class="col-sm-9">
<small>If you are using Bower just run:</small>
<pre style="margin-bottom:0;">bower install angular-bootstrap</pre>
<small class="help-block"><a href="http://bower.io/" target="_blank">Bower</a> is a package manager for the web.</small>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<a class="btn btn-default" ng-click="cancel()">Close</a>
<a class="btn btn-primary" ng-href="{{download('<%= pkg.version%>')}}" download><i class="glyphicon glyphicon-download-alt"></i> Download <%= pkg.version %></a>
</div>
</script>
<script type="text/ng-template" id="buildModal.html">
<div class="modal-header">
<h4>
Create a Custom Build
<br>
<small>Select the modules you wish to download</small>
</h4>
</div>
<div class="modal-body">
<div ng-show="isOldBrowser()">
Your current browser doesn't support creating custom builds.
Please take a second to <a href="http://browsehappy.com/">upgrade to a
more modern browser</a> (other than Safari).
</div>
<div ng-show="buildErrorText">
<h4 style="text-align: center;">{{buildErrorText}}</h4>
</div>
<div ng-hide="buildErrorText || isOldBrowser()">
<% modules.forEach(function(module,i) { %>
<% if (i % 3 === 0) {%>
<div class="btn-group" style="width: 100%;">
<% } %>
<button type="button" class="btn btn-default"
style="width: 33%; border-radius: 0;"
ng-class="{'btn-primary': module.<%= module.name %>}"
ng-model="module.<%= module.name %>"
btn-checkbox
ng-change="selectedChanged('<%= module.name %>', module.<%= module.name %>)">
<%= module.displayName %>
</button>
<% if ((i+1) % 3 === 0) { //end button group%>
</div>
<% } %>
<% }); %>
</div>
</div>
<div class="modal-footer">
<a class="btn btn-default" ng-click="cancel()">Close</a>
<a class="btn btn-primary" ng-hide="isOldBrowser()"
ng-disabled="isOldBrowser() !== false && !selectedModules.length"
ng-click="selectedModules.length && build(selectedModules, '<%= pkg.version %>')">
<i class="glyphicon glyphicon-download-alt"></i> Download {{selectedModules.length}} Modules
</a>
</div>
</script>
<script type="text/javascript">
var _gaq = _gaq || [];
_gaq.push(['_setAccount', 'UA-37467169-1']);
_gaq.push(['_trackPageview']);
(function() {
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
})();
</script>
<script src="assets/smoothscroll-angular-custom.js"></script>
<script src="assets/uglifyjs.js"></script>
</body>
</html>

View file

@ -0,0 +1,41 @@
/*!
* 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) {
if (!banner) {
banner = '';
}
var filesJsObject = {
banner: banner,
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 diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,18 @@
// jasmine matcher for expecting an element to have a css class
// https://github.com/angular/angular.js/blob/master/test/matchers.js
beforeEach(function() {
this.addMatchers({
toHaveClass: function(cls) {
this.message = function() {
return "Expected '" + this.actual + "'" + (this.isNot ? ' not ' : ' ') + "to have class '" + cls + "'.";
};
return this.actual.hasClass(cls);
},
toBeHidden: function () {
var element = angular.element(this.actual);
return element.hasClass('ng-hide') ||
element.css('display') == 'none';
}
});
});

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,106 @@
#!/usr/bin/env node
/**
* Git COMMIT-MSG hook for validating commit message
* See https://docs.google.com/document/d/1rk04jEuGfk9kYzfqCuOlPTSJw3hEDZJTBN5E5f1SALo/edit
*
* Installation:
* >> cd <angular-repo>
* >> 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 "<type>(<scope>): <subject>" ! was: "' + message + '"\nNote: <scope> 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);
}
});
}

View file

@ -0,0 +1,24 @@
{
"author": "https://github.com/angular-ui/bootstrap/graphs/contributors",
"name": "angular-ui-bootstrap",
"version": "0.12.1",
"homepage": "http://angular-ui.github.io/bootstrap/",
"dependencies": {},
"devDependencies": {
"grunt": "~0.4.1",
"grunt-ngdocs": "~0.1.1",
"grunt-conventional-changelog": "~0.1.2",
"grunt-contrib-concat": "~0.3.0",
"grunt-contrib-copy": "~0.5.0",
"grunt-contrib-uglify": "~0.3.0",
"grunt-contrib-watch": "~0.5.0",
"grunt-contrib-jshint": "~0.8.0",
"grunt-html2js": "~0.2.0",
"grunt-karma": "~0.4.4",
"node-markdown": "0.1.1",
"semver": "~2.2.0",
"shelljs": "~0.2.0",
"grunt-ddescribe-iit": "0.0.4"
},
"license": "MIT"
}

View file

@ -0,0 +1,130 @@
angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse'])
.constant('accordionConfig', {
closeOthers: true
})
.controller('AccordionController', ['$scope', '$attrs', 'accordionConfig', 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('accordion', function () {
return {
restrict:'EA',
controller:'AccordionController',
transclude: true,
replace: false,
templateUrl: 'template/accordion/accordion.html'
};
})
// The accordion-group directive indicates a block of html that will expand and collapse in an accordion
.directive('accordionGroup', function() {
return {
require:'^accordion', // We need this directive to be inside an accordion
restrict:'EA',
transclude:true, // It transcludes the contents of the directive into the template
replace: true, // The element containing the directive will be replaced with the template
templateUrl:'template/accordion/accordion-group.html',
scope: {
heading: '@', // Interpolate the heading attribute onto this scope
isOpen: '=?',
isDisabled: '=?'
},
controller: function() {
this.setHeading = function(element) {
this.heading = element;
};
},
link: function(scope, element, attrs, accordionCtrl) {
accordionCtrl.addGroup(scope);
scope.$watch('isOpen', function(value) {
if ( value ) {
accordionCtrl.closeOthers(scope);
}
});
scope.toggleOpen = function() {
if ( !scope.isDisabled ) {
scope.isOpen = !scope.isOpen;
}
};
}
};
})
// Use accordion-heading below an accordion-group to provide a heading containing HTML
// <accordion-group>
// <accordion-heading>Heading containing HTML - <img src="..."></accordion-heading>
// </accordion-group>
.directive('accordionHeading', function() {
return {
restrict: 'EA',
transclude: true, // Grab the contents to be used as the heading
template: '', // In effect remove this element!
replace: true,
require: '^accordionGroup',
link: function(scope, element, attr, 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, function() {}));
}
};
})
// 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
// <div class="accordion-group">
// <div class="accordion-heading" ><a ... accordion-transclude="heading">...</a></div>
// ...
// </div>
.directive('accordionTransclude', function() {
return {
require: '^accordionGroup',
link: function(scope, element, attr, controller) {
scope.$watch(function() { return controller[attr.accordionTransclude]; }, function(heading) {
if ( heading ) {
element.html('');
element.append(heading);
}
});
}
};
});

View file

@ -0,0 +1,30 @@
<div ng-controller="AccordionDemoCtrl">
<p>
<button class="btn btn-default btn-sm" ng-click="status.open = !status.open">Toggle last panel</button>
<button class="btn btn-default btn-sm" ng-click="status.isFirstDisabled = ! status.isFirstDisabled">Enable / Disable first panel</button>
</p>
<label class="checkbox">
<input type="checkbox" ng-model="oneAtATime">
Open only one at a time
</label>
<accordion close-others="oneAtATime">
<accordion-group heading="Static Header, initially expanded" is-open="status.isFirstOpen" is-disabled="status.isFirstDisabled">
This content is straight in the template.
</accordion-group>
<accordion-group heading="{{group.title}}" ng-repeat="group in groups">
{{group.content}}
</accordion-group>
<accordion-group heading="Dynamic Body Content">
<p>The body of the accordion group grows to fit the contents</p>
<button class="btn btn-default btn-sm" ng-click="addItem()">Add Item</button>
<div ng-repeat="item in items">{{item}}</div>
</accordion-group>
<accordion-group is-open="status.open">
<accordion-heading>
I can have markup, too! <i class="pull-right glyphicon" ng-class="{'glyphicon-chevron-down': status.open, 'glyphicon-chevron-right': !status.open}"></i>
</accordion-heading>
This is just some content to illustrate fancy headings.
</accordion-group>
</accordion>
</div>

View file

@ -0,0 +1,26 @@
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 = {
isFirstOpen: true,
isFirstDisabled: false
};
});

View file

@ -0,0 +1,5 @@
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.
We can control whether expanding an item will cause the other items to close, using the `close-others` attribute on accordion.
The body of each accordion group is transcluded in to the body of the collapsible element.

View file

@ -0,0 +1,414 @@
describe('accordion', function () {
var $scope;
beforeEach(module('ui.bootstrap.accordion'));
beforeEach(module('template/accordion/accordion.html'));
beforeEach(module('template/accordion/accordion-group.html'));
beforeEach(inject(function ($rootScope) {
$scope = $rootScope;
}));
describe('controller', function () {
var ctrl, $element, $attrs;
beforeEach(inject(function($controller) {
$attrs = {}; $element = {};
ctrl = $controller('AccordionController', { $scope: $scope, $element: $element, $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(accordionConfig) {
originalCloseOthers = accordionConfig.closeOthers;
accordionConfig.closeOthers = false;
}));
afterEach(inject(function(accordionConfig) {
// return it to the original value
accordionConfig.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);
});
});
});
describe('accordion-group', function () {
var scope, $compile;
var element, groups;
var findGroupLink = function (index) {
return groups.eq(index).find('a').eq(0);
};
var findGroupBody = function (index) {
return groups.eq(index).find('.panel-collapse').eq(0);
};
beforeEach(inject(function(_$rootScope_, _$compile_) {
scope = _$rootScope_;
$compile = _$compile_;
}));
afterEach(function () {
element = groups = scope = $compile = undefined;
});
describe('with static panels', function () {
beforeEach(function () {
var tpl =
'<accordion>' +
'<accordion-group heading="title 1">Content 1</accordion-group>' +
'<accordion-group heading="title 2">Content 2</accordion-group>' +
'</accordion>';
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);
findGroupLink(1).click();
scope.$digest();
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);
findGroupLink(0).click();
scope.$digest();
expect(findGroupBody(0).scope().isOpen).toBe(false);
});
});
describe('with dynamic panels', function () {
var model;
beforeEach(function () {
var tpl =
'<accordion>' +
'<accordion-group ng-repeat="group in groups" heading="{{group.name}}">{{group.content}}</accordion-group>' +
'</accordion>';
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 =
'<accordion>' +
'<accordion-group heading="title 1" is-open="open.first">Content 1</accordion-group>' +
'<accordion-group heading="title 2" is-open="open.second">Content 2</accordion-group>' +
'</accordion>';
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 =
'<accordion>' +
'<accordion-group heading="title 1" is-open="open1"><div ng-repeat="item in items">{{item}}</div></accordion-group>' +
'<accordion-group heading="title 2" is-open="open2">Static content</accordion-group>' +
'</accordion>';
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();
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)[0].clientHeight).not.toBe(0);
expect(findGroupBody(1)[0].clientHeight).toBe(0);
});
});
describe('is-open attribute with dynamic groups', function () {
beforeEach(function () {
var tpl =
'<accordion>' +
'<accordion-group ng-repeat="group in groups" heading="{{group.name}}" is-open="group.open">{{group.content}}</accordion-group>' +
'</accordion>';
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-disabled` attribute', function() {
var groupBody;
beforeEach(function () {
var tpl =
'<accordion>' +
'<accordion-group heading="title 1" is-disabled="disabled">Content 1</accordion-group>' +
'</accordion>';
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();
});
});
describe('accordion-heading element', function() {
beforeEach(function() {
var tpl =
'<accordion ng-init="a = [1,2,3]">' +
'<accordion-group heading="I get overridden">' +
'<accordion-heading>Heading Element <span ng-repeat="x in a">{{x}}</span> </accordion-heading>' +
'Body' +
'</accordion-group>' +
'</accordion>';
element = $compile(tpl)(scope);
scope.$digest();
groups = element.find('.panel');
});
it('transcludes the <accordion-heading> 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).find('span').scope().$id).toBe(findGroupBody(0).find('span').scope().$id);
});
});
describe('accordion-heading attribute', function() {
beforeEach(function() {
var tpl =
'<accordion ng-init="a = [1,2,3]">' +
'<accordion-group heading="I get overridden">' +
'<div accordion-heading>Heading Element <span ng-repeat="x in a">{{x}}</span> </div>' +
'Body' +
'</accordion-group>' +
'</accordion>';
element = $compile(tpl)(scope);
scope.$digest();
groups = element.find('.panel');
});
it('transcludes the <accordion-heading> 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).find('span').scope().$id).toBe(findGroupBody(0).find('span').scope().$id);
});
});
describe('accordion-heading, with repeating accordion-groups', function() {
it('should clone the accordion-heading for each group', function() {
element = $compile('<accordion><accordion-group ng-repeat="x in [1,2,3]"><accordion-heading>{{x}}</accordion-heading></accordion-group></accordion>')(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('accordion-heading attribute, with repeating accordion-groups', function() {
it('should clone the accordion-heading for each group', function() {
element = $compile('<accordion><accordion-group ng-repeat="x in [1,2,3]"><div accordion-heading>{{x}}</div></accordion-group></accordion>')(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');
});
});
});
});

View file

@ -0,0 +1,31 @@
angular.module('ui.bootstrap.alert', [])
.controller('AlertController', ['$scope', '$attrs', function ($scope, $attrs) {
$scope.closeable = 'close' in $attrs;
this.close = $scope.close;
}])
.directive('alert', function () {
return {
restrict:'EA',
controller:'AlertController',
templateUrl:'template/alert/alert.html',
transclude:true,
replace:true,
scope: {
type: '@',
close: '&'
}
};
})
.directive('dismissOnTimeout', ['$timeout', function($timeout) {
return {
require: 'alert',
link: function(scope, element, attrs, alertCtrl) {
$timeout(function(){
alertCtrl.close();
}, parseInt(attrs.dismissOnTimeout, 10));
}
};
}]);

View file

@ -0,0 +1,4 @@
<div ng-controller="AlertDemoCtrl">
<alert ng-repeat="alert in alerts" type="{{alert.type}}" close="closeAlert($index)">{{alert.msg}}</alert>
<button class='btn btn-default' ng-click="addAlert()">Add Alert</button>
</div>

View file

@ -0,0 +1,14 @@
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);
};
});

View file

@ -0,0 +1,5 @@
Alert is an AngularJS-version of bootstrap's alert.
This directive can be used to generate alerts from the dynamic model data (using the `ng-repeat` directive);
The presence of the `close` attribute determines if a close button is displayed

View file

@ -0,0 +1,108 @@
describe('alert', function () {
var scope, $compile;
var element;
beforeEach(module('ui.bootstrap.alert'));
beforeEach(module('template/alert/alert.html'));
beforeEach(inject(function ($rootScope, _$compile_) {
scope = $rootScope;
$compile = _$compile_;
element = angular.element(
'<div>' +
'<alert ng-repeat="alert in alerts" type="{{alert.type}}"' +
'close="removeAlert($index)">{{alert.msg}}' +
'</alert>' +
'</div>');
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] span').eq(index);
}
it('should generate alerts using ng-repeat', function () {
var alerts = createAlerts();
expect(alerts.length).toEqual(3);
});
it('should use correct classes for different alert types', function () {
var alerts = createAlerts();
expect(alerts.eq(0)).toHaveClass('alert-success');
expect(alerts.eq(1)).toHaveClass('alert-error');
expect(alerts.eq(2)).toHaveClass('alert-warning');
});
it('should respect alert type binding', function () {
var alerts = createAlerts();
expect(alerts.eq(0)).toHaveClass('alert-success');
scope.alerts[0].type = 'error';
scope.$digest();
expect(alerts.eq(0)).toHaveClass('alert-error');
});
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 dismissable 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-dismissable');
}
});
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 dismissable class if no close callback specified', function () {
element = $compile('<alert>No close</alert>')(scope);
scope.$digest();
expect(findCloseButton(0)).toBeHidden();
expect(element).not.toHaveClass('alert-dismissable');
});
it('should be possible to add additional classes for alert', function () {
var element = $compile('<alert class="alert-block" type="info">Default alert!</alert>')(scope);
scope.$digest();
expect(element).toHaveClass('alert-block');
expect(element).toHaveClass('alert-info');
});
});

View file

@ -0,0 +1,21 @@
describe('dismissOnTimeout', function () {
var scope, $compile, $timeout;
beforeEach(module('ui.bootstrap.alert'));
beforeEach(module('template/alert/alert.html'));
beforeEach(inject(function ($rootScope, _$compile_, _$timeout_) {
scope = $rootScope;
$compile = _$compile_;
$timeout = _$timeout_;
}));
it('should close automatically if auto-dismiss is defined on the element', function () {
scope.removeAlert = jasmine.createSpy();
$compile('<alert close="removeAlert()" dismiss-on-timeout="500">Default alert!</alert>')(scope);
scope.$digest();
$timeout.flush();
expect(scope.removeAlert).toHaveBeenCalled();
});
});

View file

@ -0,0 +1,10 @@
angular.module('ui.bootstrap.bindHtml', [])
.directive('bindHtmlUnsafe', function () {
return function (scope, element, attr) {
element.addClass('ng-binding').data('$binding', attr.bindHtmlUnsafe);
scope.$watch(attr.bindHtmlUnsafe, function bindHtmlUnsafeWatchAction(value) {
element.html(value || '');
});
};
});

View file

@ -0,0 +1,74 @@
angular.module('ui.bootstrap.buttons', [])
.constant('buttonConfig', {
activeClass: 'active',
toggleEvent: 'click'
})
.controller('ButtonsController', ['buttonConfig', function(buttonConfig) {
this.activeClass = buttonConfig.activeClass || 'active';
this.toggleEvent = buttonConfig.toggleEvent || 'click';
}])
.directive('btnRadio', function () {
return {
require: ['btnRadio', 'ngModel'],
controller: 'ButtonsController',
link: function (scope, element, attrs, ctrls) {
var buttonsCtrl = ctrls[0], ngModelCtrl = ctrls[1];
//model -> UI
ngModelCtrl.$render = function () {
element.toggleClass(buttonsCtrl.activeClass, angular.equals(ngModelCtrl.$modelValue, scope.$eval(attrs.btnRadio)));
};
//ui->model
element.bind(buttonsCtrl.toggleEvent, function () {
var isActive = element.hasClass(buttonsCtrl.activeClass);
if (!isActive || angular.isDefined(attrs.uncheckable)) {
scope.$apply(function () {
ngModelCtrl.$setViewValue(isActive ? null : scope.$eval(attrs.btnRadio));
ngModelCtrl.$render();
});
}
});
}
};
})
.directive('btnCheckbox', function () {
return {
require: ['btnCheckbox', 'ngModel'],
controller: 'ButtonsController',
link: function (scope, element, attrs, ctrls) {
var buttonsCtrl = ctrls[0], ngModelCtrl = ctrls[1];
function getTrueValue() {
return getCheckboxValue(attrs.btnCheckboxTrue, true);
}
function getFalseValue() {
return getCheckboxValue(attrs.btnCheckboxFalse, false);
}
function getCheckboxValue(attributeValue, defaultValue) {
var val = scope.$eval(attributeValue);
return angular.isDefined(val) ? val : defaultValue;
}
//model -> UI
ngModelCtrl.$render = function () {
element.toggleClass(buttonsCtrl.activeClass, angular.equals(ngModelCtrl.$modelValue, getTrueValue()));
};
//ui->model
element.bind(buttonsCtrl.toggleEvent, function () {
scope.$apply(function () {
ngModelCtrl.$setViewValue(element.hasClass(buttonsCtrl.activeClass) ? getFalseValue() : getTrueValue());
ngModelCtrl.$render();
});
});
}
};
});

View file

@ -0,0 +1,26 @@
<div ng-controller="ButtonsCtrl">
<h4>Single toggle</h4>
<pre>{{singleModel}}</pre>
<button type="button" class="btn btn-primary" ng-model="singleModel" btn-checkbox btn-checkbox-true="1" btn-checkbox-false="0">
Single Toggle
</button>
<h4>Checkbox</h4>
<pre>{{checkModel}}</pre>
<div class="btn-group">
<label class="btn btn-primary" ng-model="checkModel.left" btn-checkbox>Left</label>
<label class="btn btn-primary" ng-model="checkModel.middle" btn-checkbox>Middle</label>
<label class="btn btn-primary" ng-model="checkModel.right" btn-checkbox>Right</label>
</div>
<h4>Radio &amp; Uncheckable Radio</h4>
<pre>{{radioModel || 'null'}}</pre>
<div class="btn-group">
<label class="btn btn-primary" ng-model="radioModel" btn-radio="'Left'">Left</label>
<label class="btn btn-primary" ng-model="radioModel" btn-radio="'Middle'">Middle</label>
<label class="btn btn-primary" ng-model="radioModel" btn-radio="'Right'">Right</label>
</div>
<div class="btn-group">
<label class="btn btn-success" ng-model="radioModel" btn-radio="'Left'" uncheckable>Left</label>
<label class="btn btn-success" ng-model="radioModel" btn-radio="'Middle'" uncheckable>Middle</label>
<label class="btn btn-success" ng-model="radioModel" btn-radio="'Right'" uncheckable>Right</label>
</div>
</div>

View file

@ -0,0 +1,11 @@
angular.module('ui.bootstrap.demo').controller('ButtonsCtrl', function ($scope) {
$scope.singleModel = 1;
$scope.radioModel = 'Middle';
$scope.checkModel = {
left: false,
middle: true,
right: false
};
});

View file

@ -0,0 +1 @@
There are two directives that can make a group of buttons behave like a set of checkboxes, radio buttons, or a hybrid where radio buttons can be unchecked.

View file

@ -0,0 +1,188 @@
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;
};
//model -> UI
it('should work correctly with default model values', function () {
$scope.model = false;
var btn = compileButton('<button ng-model="model" btn-checkbox>click</button>', $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('<button ng-model="model" btn-checkbox btn-checkbox-true="1" btn-checkbox-false="0">click</button>', $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('<button ng-model="model" btn-checkbox>click</button>', $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('<button ng-model="model" btn-checkbox btn-checkbox-true="1" btn-checkbox-false="0">click</button>', $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('<button ng-model="model" btn-checkbox btn-checkbox-true="trueVal">click</button>', $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);
});
});
describe('radio', function () {
var compileButtons = function (markup, scope) {
var el = $compile('<div>'+markup+'</div>')(scope);
scope.$digest();
return el.find('button');
};
//model -> UI
it('should work correctly set active class based on model', function () {
var btns = compileButtons('<button ng-model="model" btn-radio="1">click1</button><button ng-model="model" btn-radio="2">click2</button>', $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 work correctly set active class based on model', function () {
var btns = compileButtons('<button ng-model="model" btn-radio="1">click1</button><button ng-model="model" btn-radio="2">click2</button>', $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 btn-radio values and update state accordingly', function () {
$scope.values = ['value1', 'value2'];
var btns = compileButtons('<button ng-model="model" btn-radio="values[0]">click1</button><button ng-model="model" btn-radio="values[1]">click2</button>', $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');
});
describe('uncheckable', function () {
//model -> UI
it('should set active class based on model', function () {
var btns = compileButtons('<button ng-model="model" btn-radio="1" uncheckable>click1</button><button ng-model="model" btn-radio="2" uncheckable>click2</button>', $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 based on model', function () {
var btns = compileButtons('<button ng-model="model" btn-radio="1" uncheckable>click1</button><button ng-model="model" btn-radio="2" uncheckable>click2</button>', $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).toEqual(undefined);
expect(btns.eq(1)).not.toHaveClass('active');
expect(btns.eq(0)).not.toHaveClass('active');
});
it('should watch btn-radio values and update state', function () {
$scope.values = ['value1', 'value2'];
var btns = compileButtons('<button ng-model="model" btn-radio="values[0]" uncheckable>click1</button><button ng-model="model" btn-radio="values[1]" uncheckable>click2</button>', $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');
});
});
});
});

View file

@ -0,0 +1,293 @@
/**
* @ngdoc overview
* @name ui.bootstrap.carousel
*
* @description
* AngularJS version of an image carousel.
*
*/
angular.module('ui.bootstrap.carousel', ['ui.bootstrap.transition'])
.controller('CarouselController', ['$scope', '$timeout', '$interval', '$transition', function ($scope, $timeout, $interval, $transition) {
var self = this,
slides = self.slides = $scope.slides = [],
currentIndex = -1,
currentInterval, isPlaying;
self.currentSlide = null;
var destroyed = false;
/* direction: "prev" or "next" */
self.select = $scope.select = function(nextSlide, direction) {
var nextIndex = slides.indexOf(nextSlide);
//Decide direction if it's not given
if (direction === undefined) {
direction = nextIndex > currentIndex ? 'next' : 'prev';
}
if (nextSlide && nextSlide !== self.currentSlide) {
if ($scope.$currentTransition) {
$scope.$currentTransition.cancel();
//Timeout so ng-class in template has time to fix classes for finished slide
$timeout(goNext);
} else {
goNext();
}
}
function goNext() {
// Scope has been destroyed, stop here.
if (destroyed) { return; }
//If we have a slide to transition from and we have a transition type and we're allowed, go
if (self.currentSlide && angular.isString(direction) && !$scope.noTransition && nextSlide.$element) {
//We shouldn't do class manip in here, but it's the same weird thing bootstrap does. need to fix sometime
nextSlide.$element.addClass(direction);
var reflow = nextSlide.$element[0].offsetWidth; //force reflow
//Set all other slides to stop doing their stuff for the new transition
angular.forEach(slides, function(slide) {
angular.extend(slide, {direction: '', entering: false, leaving: false, active: false});
});
angular.extend(nextSlide, {direction: direction, active: true, entering: true});
angular.extend(self.currentSlide||{}, {direction: direction, leaving: true});
$scope.$currentTransition = $transition(nextSlide.$element, {});
//We have to create new pointers inside a closure since next & current will change
(function(next,current) {
$scope.$currentTransition.then(
function(){ transitionDone(next, current); },
function(){ transitionDone(next, current); }
);
}(nextSlide, self.currentSlide));
} else {
transitionDone(nextSlide, self.currentSlide);
}
self.currentSlide = nextSlide;
currentIndex = nextIndex;
//every time you change slides, reset the timer
restartTimer();
}
function transitionDone(next, current) {
angular.extend(next, {direction: '', active: true, leaving: false, entering: false});
angular.extend(current||{}, {direction: '', active: false, leaving: false, entering: false});
$scope.$currentTransition = null;
}
};
$scope.$on('$destroy', function () {
destroyed = true;
});
/* Allow outside people to call indexOf on slides array */
self.indexOfSlide = function(slide) {
return slides.indexOf(slide);
};
$scope.next = function() {
var newIndex = (currentIndex + 1) % slides.length;
//Prevent this user-triggered transition from occurring if there is already one in progress
if (!$scope.$currentTransition) {
return self.select(slides[newIndex], 'next');
}
};
$scope.prev = function() {
var newIndex = currentIndex - 1 < 0 ? slides.length - 1 : currentIndex - 1;
//Prevent this user-triggered transition from occurring if there is already one in progress
if (!$scope.$currentTransition) {
return self.select(slides[newIndex], 'prev');
}
};
$scope.isActive = function(slide) {
return self.currentSlide === slide;
};
$scope.$watch('interval', restartTimer);
$scope.$on('$destroy', resetTimer);
function restartTimer() {
resetTimer();
var interval = +$scope.interval;
if (!isNaN(interval) && interval > 0) {
currentInterval = $interval(timerFn, interval);
}
}
function resetTimer() {
if (currentInterval) {
$interval.cancel(currentInterval);
currentInterval = null;
}
}
function timerFn() {
var interval = +$scope.interval;
if (isPlaying && !isNaN(interval) && interval > 0) {
$scope.next();
} else {
$scope.pause();
}
}
$scope.play = function() {
if (!isPlaying) {
isPlaying = true;
restartTimer();
}
};
$scope.pause = function() {
if (!$scope.noPause) {
isPlaying = false;
resetTimer();
}
};
self.addSlide = function(slide, element) {
slide.$element = element;
slides.push(slide);
//if this is the first slide or the slide is set to active, select it
if(slides.length === 1 || slide.active) {
self.select(slides[slides.length-1]);
if (slides.length == 1) {
$scope.play();
}
} else {
slide.active = false;
}
};
self.removeSlide = function(slide) {
//get the index of the slide inside the carousel
var index = slides.indexOf(slide);
slides.splice(index, 1);
if (slides.length > 0 && slide.active) {
if (index >= slides.length) {
self.select(slides[index-1]);
} else {
self.select(slides[index]);
}
} else if (currentIndex > index) {
currentIndex--;
}
};
}])
/**
* @ngdoc directive
* @name ui.bootstrap.carousel.directive:carousel
* @restrict EA
*
* @description
* Carousel is the outer container for a set of image 'slides' to showcase.
*
* @param {number=} interval The time, in milliseconds, that it will take the carousel to go to the next slide.
* @param {boolean=} noTransition Whether to disable transitions on the carousel.
* @param {boolean=} noPause Whether to disable pausing on the carousel (by default, the carousel interval pauses on hover).
*
* @example
<example module="ui.bootstrap">
<file name="index.html">
<carousel>
<slide>
<img src="http://placekitten.com/150/150" style="margin:auto;">
<div class="carousel-caption">
<p>Beautiful!</p>
</div>
</slide>
<slide>
<img src="http://placekitten.com/100/150" style="margin:auto;">
<div class="carousel-caption">
<p>D'aww!</p>
</div>
</slide>
</carousel>
</file>
<file name="demo.css">
.carousel-indicators {
top: auto;
bottom: 15px;
}
</file>
</example>
*/
.directive('carousel', [function() {
return {
restrict: 'EA',
transclude: true,
replace: true,
controller: 'CarouselController',
require: 'carousel',
templateUrl: 'template/carousel/carousel.html',
scope: {
interval: '=',
noTransition: '=',
noPause: '='
}
};
}])
/**
* @ngdoc directive
* @name ui.bootstrap.carousel.directive:slide
* @restrict EA
*
* @description
* Creates a slide inside a {@link ui.bootstrap.carousel.directive:carousel carousel}. Must be placed as a child of a carousel element.
*
* @param {boolean=} active Model binding, whether or not this slide is currently active.
*
* @example
<example module="ui.bootstrap">
<file name="index.html">
<div ng-controller="CarouselDemoCtrl">
<carousel>
<slide ng-repeat="slide in slides" active="slide.active">
<img ng-src="{{slide.image}}" style="margin:auto;">
<div class="carousel-caption">
<h4>Slide {{$index}}</h4>
<p>{{slide.text}}</p>
</div>
</slide>
</carousel>
Interval, in milliseconds: <input type="number" ng-model="myInterval">
<br />Enter a negative number to stop the interval.
</div>
</file>
<file name="script.js">
function CarouselDemoCtrl($scope) {
$scope.myInterval = 5000;
}
</file>
<file name="demo.css">
.carousel-indicators {
top: auto;
bottom: 15px;
}
</file>
</example>
*/
.directive('slide', function() {
return {
require: '^carousel',
restrict: 'EA',
transclude: true,
replace: true,
templateUrl: 'template/carousel/slide.html',
scope: {
active: '=?'
},
link: function (scope, element, attrs, carouselCtrl) {
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) {
if (active) {
carouselCtrl.select(scope);
}
});
}
};
});

View file

@ -0,0 +1,5 @@
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 `<carousel>` element with `<slide>` elements inside it. It will automatically cycle through the slides at a given rate, and a current-index variable will be kept in sync with the currently visible slide.

View file

@ -0,0 +1,22 @@
<div ng-controller="CarouselDemoCtrl">
<div style="height: 305px">
<carousel interval="myInterval">
<slide ng-repeat="slide in slides" active="slide.active">
<img ng-src="{{slide.image}}" style="margin:auto;">
<div class="carousel-caption">
<h4>Slide {{$index}}</h4>
<p>{{slide.text}}</p>
</div>
</slide>
</carousel>
</div>
<div class="row">
<div class="col-md-6">
<button type="button" class="btn btn-info" ng-click="addSlide()">Add Slide</button>
</div>
<div class="col-md-6">
Interval, in milliseconds: <input type="number" class="form-control" ng-model="myInterval">
<br />Enter a negative number or 0 to stop the interval.
</div>
</div>
</div>

View file

@ -0,0 +1,15 @@
angular.module('ui.bootstrap.demo').controller('CarouselDemoCtrl', function ($scope) {
$scope.myInterval = 5000;
var slides = $scope.slides = [];
$scope.addSlide = function() {
var newWidth = 600 + slides.length + 1;
slides.push({
image: 'http://placekitten.com/' + newWidth + '/300',
text: ['More','Extra','Lots of','Surplus'][slides.length % 4] + ' ' +
['Cats', 'Kittys', 'Felines', 'Cutes'][slides.length % 4]
});
};
for (var i=0; i<4; i++) {
$scope.addSlide();
}
});

View file

@ -0,0 +1,343 @@
describe('carousel', function() {
beforeEach(module('ui.bootstrap.carousel', function($compileProvider, $provide) {
angular.forEach(['ngSwipeLeft', 'ngSwipeRight'], makeMock);
function makeMock(name) {
$provide.value(name + 'Directive', []); //remove existing directive if it exists
$compileProvider.directive(name, function() {
return function(scope, element, attr) {
element.on(name, function() {
scope.$apply(attr[name]);
});
};
});
}
}));
beforeEach(module('template/carousel/carousel.html', 'template/carousel/slide.html'));
var $rootScope, $compile, $controller, $interval;
beforeEach(inject(function(_$rootScope_, _$compile_, _$controller_, _$interval_) {
$rootScope = _$rootScope_;
$compile = _$compile_;
$controller = _$controller_;
$interval = _$interval_;
}));
describe('basics', function() {
var elm, scope;
beforeEach(function() {
scope = $rootScope.$new();
scope.slides = [
{active:false,content:'one'},
{active:false,content:'two'},
{active:false,content:'three'}
];
elm = $compile(
'<carousel interval="interval" no-transition="true" no-pause="nopause">' +
'<slide ng-repeat="slide in slides" active="slide.active">' +
'{{slide.content}}' +
'</slide>' +
'</carousel>'
)(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.slides[i].active).toBe(true);
} else {
expect(scope.slides[i].active).not.toBe(true);
}
}
}
it('should set the selected slide to active = true', function() {
expect(scope.slides[0].content).toBe('one');
testSlideActive(0);
scope.$apply('slides[1].active=true');
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 hide navigation when only one slide', function () {
scope.slides=[{active:false,content:'one'}];
scope.$apply();
elm = $compile(
'<carousel interval="interval" no-transition="true">' +
'<slide ng-repeat="slide in slides" active="slide.active">' +
'{{slide.content}}' +
'</slide>' +
'</carousel>'
)(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 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);
});
describe('swiping', function() {
it('should go next on swipeLeft', function() {
testSlideActive(0);
elm.triggerHandler('ngSwipeLeft');
testSlideActive(1);
});
it('should go prev on swipeRight', function() {
testSlideActive(0);
elm.triggerHandler('ngSwipeRight');
testSlideActive(2);
});
});
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', 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);
});
it('should bind the content to slides', function() {
var contents = elm.find('div.item');
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('slides[2].active = true');
testSlideActive(2);
scope.$apply('slides.splice(0,1)');
expect(elm.find('div.item').length).toBe(2);
testSlideActive(1);
$interval.flush(scope.interval);
testSlideActive(0);
scope.$apply('slides.splice(1,1)');
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'},{content:'new2'},{content:'new3'}];
scope.$apply();
var contents = elm.find('div.item');
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('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').andCallThrough();
scope.$destroy();
expect($interval.cancel).toHaveBeenCalled();
});
});
describe('controller', function() {
var scope, ctrl;
//create an array of slides and add to the scope
var slides = [{'content':1},{'content':2},{'content':3},{'content':4}];
beforeEach(function() {
scope = $rootScope.$new();
ctrl = $controller('CarouselController', {$scope: scope, $element: null});
for(var i = 0;i < slides.length;i++){
ctrl.addSlide(slides[i]);
}
});
describe('addSlide', function() {
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.active).not.toBe(true);
} else {
expect(slide.active).toBe(true);
}
});
});
it('should add new slide and change active to true if active is true on the added slide', function() {
var newSlide = {active: true};
expect(ctrl.slides.length).toBe(4);
ctrl.addSlide(newSlide);
expect(ctrl.slides.length).toBe(5);
expect(ctrl.slides[4].active).toBe(true);
expect(ctrl.slides[0].active).toBe(false);
});
it('should add a new slide and not change the active slide', function() {
var newSlide = {active: false};
expect(ctrl.slides.length).toBe(4);
ctrl.addSlide(newSlide);
expect(ctrl.slides.length).toBe(5);
expect(ctrl.slides[4].active).toBe(false);
expect(ctrl.slides[0].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]);
expect(ctrl.slides.length).toBe(3);
expect(ctrl.currentSlide).toBe(ctrl.slides[0]);
ctrl.select(ctrl.slides[2]);
ctrl.removeSlide(ctrl.slides[2]);
expect(ctrl.slides.length).toBe(2);
expect(ctrl.currentSlide).toBe(ctrl.slides[1]);
ctrl.removeSlide(ctrl.slides[0]);
expect(ctrl.slides.length).toBe(1);
expect(ctrl.currentSlide).toBe(ctrl.slides[0]);
});
it('issue 1414 - should not continue running timers after scope is destroyed', function() {
spyOn(scope, 'next').andCallThrough();
scope.interval = 2000;
scope.$digest();
$interval.flush(scope.interval);
expect(scope.next.calls.length).toBe(1);
scope.$destroy();
$interval.flush(scope.interval);
expect(scope.next.calls.length).toBe(1);
});
});
});
});

View file

@ -0,0 +1,75 @@
angular.module('ui.bootstrap.collapse', ['ui.bootstrap.transition'])
.directive('collapse', ['$transition', function ($transition) {
return {
link: function (scope, element, attrs) {
var initialAnimSkip = true;
var currentTransition;
function doTransition(change) {
var newTransition = $transition(element, change);
if (currentTransition) {
currentTransition.cancel();
}
currentTransition = newTransition;
newTransition.then(newTransitionDone, newTransitionDone);
return newTransition;
function newTransitionDone() {
// Make sure it's this transition, otherwise, leave it alone.
if (currentTransition === newTransition) {
currentTransition = undefined;
}
}
}
function expand() {
if (initialAnimSkip) {
initialAnimSkip = false;
expandDone();
} else {
element.removeClass('collapse').addClass('collapsing');
doTransition({ height: element[0].scrollHeight + 'px' }).then(expandDone);
}
}
function expandDone() {
element.removeClass('collapsing');
element.addClass('collapse in');
element.css({height: 'auto'});
}
function collapse() {
if (initialAnimSkip) {
initialAnimSkip = false;
collapseDone();
element.css({height: 0});
} else {
// CSS transitions don't work with height: auto, so we have to manually change the height to a specific value
element.css({ height: element[0].scrollHeight + 'px' });
//trigger reflow so a browser realizes that height was updated from auto to a specific value
var x = element[0].offsetWidth;
element.removeClass('collapse in').addClass('collapsing');
doTransition({ height: 0 }).then(collapseDone);
}
}
function collapseDone() {
element.removeClass('collapsing');
element.addClass('collapse');
}
scope.$watch(attrs.collapse, function (shouldCollapse) {
if (shouldCollapse) {
collapse();
} else {
expand();
}
});
}
};
}]);

View file

@ -0,0 +1,7 @@
<div ng-controller="CollapseDemoCtrl">
<button class="btn btn-default" ng-click="isCollapsed = !isCollapsed">Toggle collapse</button>
<hr>
<div collapse="isCollapsed">
<div class="well well-lg">Some content</div>
</div>
</div>

View file

@ -0,0 +1,3 @@
angular.module('ui.bootstrap.demo').controller('CollapseDemoCtrl', function ($scope) {
$scope.isCollapsed = false;
});

View file

@ -0,0 +1,2 @@
AngularJS version of Bootstrap's collapse plugin.
Provides a simple way to hide and show an element with a css transition

View file

@ -0,0 +1,109 @@
describe('collapse directive', function () {
var scope, $compile, $timeout, $transition;
var element;
beforeEach(module('ui.bootstrap.collapse'));
beforeEach(inject(function(_$rootScope_, _$compile_, _$timeout_, _$transition_) {
scope = _$rootScope_;
$compile = _$compile_;
$timeout = _$timeout_;
$transition = _$transition_;
}));
beforeEach(function() {
element = $compile('<div collapse="isCollapsed">Some Content</div>')(scope);
angular.element(document.body).append(element);
});
afterEach(function() {
element.remove();
});
it('should be hidden on initialization if isCollapsed = true without transition', function() {
scope.isCollapsed = true;
scope.$digest();
//No animation timeout here
expect(element.height()).toBe(0);
});
it('should collapse if isCollapsed = true with animation on subsequent use', function() {
scope.isCollapsed = false;
scope.$digest();
scope.isCollapsed = true;
scope.$digest();
$timeout.flush();
expect(element.height()).toBe(0);
});
it('should be shown on initialization if isCollapsed = false without transition', function() {
scope.isCollapsed = false;
scope.$digest();
//No animation timeout here
expect(element.height()).not.toBe(0);
});
it('should expand if isCollapsed = false with animation on subsequent use', function() {
scope.isCollapsed = false;
scope.$digest();
scope.isCollapsed = true;
scope.$digest();
scope.isCollapsed = false;
scope.$digest();
$timeout.flush();
expect(element.height()).not.toBe(0);
});
it('should expand if isCollapsed = true with animation on subsequent uses', function() {
scope.isCollapsed = false;
scope.$digest();
scope.isCollapsed = true;
scope.$digest();
scope.isCollapsed = false;
scope.$digest();
scope.isCollapsed = true;
scope.$digest();
$timeout.flush();
expect(element.height()).toBe(0);
if ($transition.transitionEndEventName) {
element.triggerHandler($transition.transitionEndEventName);
expect(element.height()).toBe(0);
}
});
describe('dynamic content', function() {
var element;
beforeEach(function() {
element = angular.element('<div collapse="isCollapsed"><p>Initial content</p><div ng-show="exp">Additional content</div></div>');
$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);
});
});
});

View file

@ -0,0 +1,126 @@
angular.module('ui.bootstrap.dateparser', [])
.service('dateParser', ['$locale', 'orderByFilter', function($locale, orderByFilter) {
this.parsers = {};
var formatCodeToRegex = {
'yyyy': {
regex: '\\d{4}',
apply: function(value) { this.year = +value; }
},
'yy': {
regex: '\\d{2}',
apply: function(value) { this.year = +value + 2000; }
},
'y': {
regex: '\\d{1,4}',
apply: function(value) { this.year = +value; }
},
'MMMM': {
regex: $locale.DATETIME_FORMATS.MONTH.join('|'),
apply: function(value) { this.month = $locale.DATETIME_FORMATS.MONTH.indexOf(value); }
},
'MMM': {
regex: $locale.DATETIME_FORMATS.SHORTMONTH.join('|'),
apply: function(value) { this.month = $locale.DATETIME_FORMATS.SHORTMONTH.indexOf(value); }
},
'MM': {
regex: '0[1-9]|1[0-2]',
apply: function(value) { this.month = value - 1; }
},
'M': {
regex: '[1-9]|1[0-2]',
apply: function(value) { this.month = value - 1; }
},
'dd': {
regex: '[0-2][0-9]{1}|3[0-1]{1}',
apply: function(value) { this.date = +value; }
},
'd': {
regex: '[1-2]?[0-9]{1}|3[0-1]{1}',
apply: function(value) { this.date = +value; }
},
'EEEE': {
regex: $locale.DATETIME_FORMATS.DAY.join('|')
},
'EEE': {
regex: $locale.DATETIME_FORMATS.SHORTDAY.join('|')
}
};
function createParser(format) {
var map = [], regex = format.split('');
angular.forEach(formatCodeToRegex, function(data, code) {
var index = format.indexOf(code);
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 + code.length; i < n; i++) {
regex[i] = '';
format[i] = '$';
}
format = format.join('');
map.push({ index: index, apply: data.apply });
}
});
return {
regex: new RegExp('^' + regex.join('') + '$'),
map: orderByFilter(map, 'index')
};
}
this.parse = function(input, format) {
if ( !angular.isString(input) || !format ) {
return input;
}
format = $locale.DATETIME_FORMATS[format] || format;
if ( !this.parsers[format] ) {
this.parsers[format] = createParser(format);
}
var parser = this.parsers[format],
regex = parser.regex,
map = parser.map,
results = input.match(regex);
if ( results && results.length ) {
var fields = { year: 1900, month: 0, date: 1, hours: 0 }, dt;
for( var i = 1, n = results.length; i < n; i++ ) {
var mapper = map[i-1];
if ( mapper.apply ) {
mapper.apply.call(fields, results[i]);
}
}
if ( isValid(fields.year, fields.month, fields.date) ) {
dt = new Date( fields.year, fields.month, fields.date, fields.hours);
}
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 ( 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;
}
}]);

View file

@ -0,0 +1,106 @@
describe('date parser', function () {
var dateParser;
beforeEach(module('ui.bootstrap.dateparser'));
beforeEach(inject(function (_dateParser_) {
dateParser = _dateParser_;
}));
function expectParse(input, format, date) {
expect(dateParser.parse(input, format)).toEqual(date);
}
describe('wih 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));
});
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(2080, 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 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 `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));
});
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));
});
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));
});
});
describe('wih 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 edge case', function() {
it('should not work for invalid number of days in February', function() {
expect(dateParser.parse('29.02.2013', 'dd.MM.yyyy')).toBeUndefined();
});
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() {
expect(dateParser.parse('31-04-2013', 'dd-MM-yyyy')).toBeUndefined();
expect(dateParser.parse('November 31, 2013', 'MMMM d, yyyy')).toBeUndefined();
});
});
it('should not parse non-string inputs', function() {
expect(dateParser.parse(123456, 'dd.MM.yyyy')).toBe(123456);
var date = new Date();
expect(dateParser.parse(date, 'dd.MM.yyyy')).toBe(date);
});
it('should not parse if no format is specified', function() {
expect(dateParser.parse('21.08.1951', '')).toBe('21.08.1951');
});
});

View file

@ -0,0 +1,639 @@
angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootstrap.position'])
.constant('datepickerConfig', {
formatDay: 'dd',
formatMonth: 'MMMM',
formatYear: 'yyyy',
formatDayHeader: 'EEE',
formatDayTitle: 'MMMM yyyy',
formatMonthTitle: 'yyyy',
datepickerMode: 'day',
minMode: 'day',
maxMode: 'year',
showWeeks: true,
startingDay: 0,
yearRange: 20,
minDate: null,
maxDate: null
})
.controller('DatepickerController', ['$scope', '$attrs', '$parse', '$interpolate', '$timeout', '$log', 'dateFilter', 'datepickerConfig', function($scope, $attrs, $parse, $interpolate, $timeout, $log, dateFilter, datepickerConfig) {
var self = this,
ngModelCtrl = { $setViewValue: angular.noop }; // nullModelCtrl;
// Modes chain
this.modes = ['day', 'month', 'year'];
// Configuration attributes
angular.forEach(['formatDay', 'formatMonth', 'formatYear', 'formatDayHeader', 'formatDayTitle', 'formatMonthTitle',
'minMode', 'maxMode', 'showWeeks', 'startingDay', 'yearRange'], function( key, index ) {
self[key] = angular.isDefined($attrs[key]) ? (index < 8 ? $interpolate($attrs[key])($scope.$parent) : $scope.$parent.$eval($attrs[key])) : datepickerConfig[key];
});
// Watchable date attributes
angular.forEach(['minDate', 'maxDate'], function( key ) {
if ( $attrs[key] ) {
$scope.$parent.$watch($parse($attrs[key]), function(value) {
self[key] = value ? new Date(value) : null;
self.refreshView();
});
} else {
self[key] = datepickerConfig[key] ? new Date(datepickerConfig[key]) : null;
}
});
$scope.datepickerMode = $scope.datepickerMode || datepickerConfig.datepickerMode;
$scope.uniqueId = 'datepicker-' + $scope.$id + '-' + Math.floor(Math.random() * 10000);
this.activeDate = angular.isDefined($attrs.initDate) ? $scope.$parent.$eval($attrs.initDate) : new Date();
$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_;
ngModelCtrl.$render = function() {
self.render();
};
};
this.render = function() {
if ( ngModelCtrl.$modelValue ) {
var date = new Date( ngModelCtrl.$modelValue ),
isValid = !isNaN(date);
if ( isValid ) {
this.activeDate = date;
} else {
$log.error('Datepicker 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.');
}
ngModelCtrl.$setValidity('date', isValid);
}
this.refreshView();
};
this.refreshView = function() {
if ( this.element ) {
this._refreshView();
var date = ngModelCtrl.$modelValue ? new Date(ngModelCtrl.$modelValue) : null;
ngModelCtrl.$setValidity('date-disabled', !date || (this.element && !this.isDisabled(date)));
}
};
this.createDateObject = function(date, format) {
var model = ngModelCtrl.$modelValue ? new Date(ngModelCtrl.$modelValue) : null;
return {
date: date,
label: dateFilter(date, format),
selected: model && this.compare(date, model) === 0,
disabled: this.isDisabled(date),
current: this.compare(date, new Date()) === 0
};
};
this.isDisabled = function( date ) {
return ((this.minDate && this.compare(date, this.minDate) < 0) || (this.maxDate && this.compare(date, this.maxDate) > 0) || ($attrs.dateDisabled && $scope.dateDisabled({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.$modelValue ? new Date( ngModelCtrl.$modelValue ) : new Date(0, 0, 0, 0, 0, 0, 0);
dt.setFullYear( date.getFullYear(), date.getMonth(), date.getDate() );
ngModelCtrl.$setViewValue( dt );
ngModelCtrl.$render();
} else {
self.activeDate = date;
$scope.datepickerMode = self.modes[ self.modes.indexOf( $scope.datepickerMode ) - 1 ];
}
};
$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;
}
$scope.datepickerMode = self.modes[ self.modes.indexOf( $scope.datepickerMode ) + direction ];
};
// 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() {
$timeout(function() {
self.element[0].focus();
}, 0 , false);
};
// Listen for focus requests from popup directive
$scope.$on('datepicker.focus', focusElement);
$scope.keydown = function( evt ) {
var key = $scope.keys[evt.which];
if ( !key || evt.shiftKey || evt.altKey ) {
return;
}
evt.preventDefault();
evt.stopPropagation();
if (key === 'enter' || key === 'space') {
if ( self.isDisabled(self.activeDate)) {
return; // do nothing
}
$scope.select(self.activeDate);
focusElement();
} else if (evt.ctrlKey && (key === 'up' || key === 'down')) {
$scope.toggleMode(key === 'up' ? 1 : -1);
focusElement();
} else {
self.handleKeyDown(key, evt);
self.refreshView();
}
};
}])
.directive( 'datepicker', function () {
return {
restrict: 'EA',
replace: true,
templateUrl: 'template/datepicker/datepicker.html',
scope: {
datepickerMode: '=?',
dateDisabled: '&'
},
require: ['datepicker', '?^ngModel'],
controller: 'DatepickerController',
link: function(scope, element, attrs, ctrls) {
var datepickerCtrl = ctrls[0], ngModelCtrl = ctrls[1];
if ( ngModelCtrl ) {
datepickerCtrl.init( ngModelCtrl );
}
}
};
})
.directive('daypicker', ['dateFilter', function (dateFilter) {
return {
restrict: 'EA',
replace: true,
templateUrl: 'template/datepicker/day.html',
require: '^datepicker',
link: function(scope, element, attrs, ctrl) {
scope.showWeeks = ctrl.showWeeks;
ctrl.step = { months: 1 };
ctrl.element = element;
var DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
function getDaysInMonth( year, month ) {
return ((month === 1) && (year % 4 === 0) && ((year % 100 !== 0) || (year % 400 === 0))) ? 29 : DAYS_IN_MONTH[month];
}
function getDates(startDate, n) {
var dates = new Array(n), current = new Date(startDate), i = 0;
current.setHours(12); // Prevent repeated dates because of timezone bug
while ( i < n ) {
dates[i++] = new Date(current);
current.setDate( current.getDate() + 1 );
}
return dates;
}
ctrl._refreshView = function() {
var year = ctrl.activeDate.getFullYear(),
month = ctrl.activeDate.getMonth(),
firstDayOfMonth = new Date(year, month, 1),
difference = ctrl.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-month calendar
var days = getDates(firstDate, 42);
for (var i = 0; i < 42; i ++) {
days[i] = angular.extend(ctrl.createDateObject(days[i], ctrl.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, ctrl.formatDayHeader),
full: dateFilter(days[j].date, 'EEEE')
};
}
scope.title = dateFilter(ctrl.activeDate, ctrl.formatDayTitle);
scope.rows = ctrl.split(days, 7);
if ( scope.showWeeks ) {
scope.weekNumbers = [];
var weekNumber = getISO8601WeekNumber( scope.rows[0][0].date ),
numWeeks = scope.rows.length;
while( scope.weekNumbers.push(weekNumber++) < numWeeks ) {}
}
};
ctrl.compare = function(date1, date2) {
return (new Date( date1.getFullYear(), date1.getMonth(), date1.getDate() ) - new Date( date2.getFullYear(), date2.getMonth(), date2.getDate() ) );
};
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;
}
ctrl.handleKeyDown = function( key, evt ) {
var date = ctrl.activeDate.getDate();
if (key === 'left') {
date = date - 1; // up
} else if (key === 'up') {
date = date - 7; // down
} else if (key === 'right') {
date = date + 1; // down
} else if (key === 'down') {
date = date + 7;
} else if (key === 'pageup' || key === 'pagedown') {
var month = ctrl.activeDate.getMonth() + (key === 'pageup' ? - 1 : 1);
ctrl.activeDate.setMonth(month, 1);
date = Math.min(getDaysInMonth(ctrl.activeDate.getFullYear(), ctrl.activeDate.getMonth()), date);
} else if (key === 'home') {
date = 1;
} else if (key === 'end') {
date = getDaysInMonth(ctrl.activeDate.getFullYear(), ctrl.activeDate.getMonth());
}
ctrl.activeDate.setDate(date);
};
ctrl.refreshView();
}
};
}])
.directive('monthpicker', ['dateFilter', function (dateFilter) {
return {
restrict: 'EA',
replace: true,
templateUrl: 'template/datepicker/month.html',
require: '^datepicker',
link: function(scope, element, attrs, ctrl) {
ctrl.step = { years: 1 };
ctrl.element = element;
ctrl._refreshView = function() {
var months = new Array(12),
year = ctrl.activeDate.getFullYear();
for ( var i = 0; i < 12; i++ ) {
months[i] = angular.extend(ctrl.createDateObject(new Date(year, i, 1), ctrl.formatMonth), {
uid: scope.uniqueId + '-' + i
});
}
scope.title = dateFilter(ctrl.activeDate, ctrl.formatMonthTitle);
scope.rows = ctrl.split(months, 3);
};
ctrl.compare = function(date1, date2) {
return new Date( date1.getFullYear(), date1.getMonth() ) - new Date( date2.getFullYear(), date2.getMonth() );
};
ctrl.handleKeyDown = function( key, evt ) {
var date = ctrl.activeDate.getMonth();
if (key === 'left') {
date = date - 1; // up
} else if (key === 'up') {
date = date - 3; // down
} else if (key === 'right') {
date = date + 1; // down
} else if (key === 'down') {
date = date + 3;
} else if (key === 'pageup' || key === 'pagedown') {
var year = ctrl.activeDate.getFullYear() + (key === 'pageup' ? - 1 : 1);
ctrl.activeDate.setFullYear(year);
} else if (key === 'home') {
date = 0;
} else if (key === 'end') {
date = 11;
}
ctrl.activeDate.setMonth(date);
};
ctrl.refreshView();
}
};
}])
.directive('yearpicker', ['dateFilter', function (dateFilter) {
return {
restrict: 'EA',
replace: true,
templateUrl: 'template/datepicker/year.html',
require: '^datepicker',
link: function(scope, element, attrs, ctrl) {
var range = ctrl.yearRange;
ctrl.step = { years: range };
ctrl.element = element;
function getStartingYear( year ) {
return parseInt((year - 1) / range, 10) * range + 1;
}
ctrl._refreshView = function() {
var years = new Array(range);
for ( var i = 0, start = getStartingYear(ctrl.activeDate.getFullYear()); i < range; i++ ) {
years[i] = angular.extend(ctrl.createDateObject(new Date(start + i, 0, 1), ctrl.formatYear), {
uid: scope.uniqueId + '-' + i
});
}
scope.title = [years[0].label, years[range - 1].label].join(' - ');
scope.rows = ctrl.split(years, 5);
};
ctrl.compare = function(date1, date2) {
return date1.getFullYear() - date2.getFullYear();
};
ctrl.handleKeyDown = function( key, evt ) {
var date = ctrl.activeDate.getFullYear();
if (key === 'left') {
date = date - 1; // up
} else if (key === 'up') {
date = date - 5; // down
} else if (key === 'right') {
date = date + 1; // down
} else if (key === 'down') {
date = date + 5;
} else if (key === 'pageup' || key === 'pagedown') {
date += (key === 'pageup' ? - 1 : 1) * ctrl.step.years;
} else if (key === 'home') {
date = getStartingYear( ctrl.activeDate.getFullYear() );
} else if (key === 'end') {
date = getStartingYear( ctrl.activeDate.getFullYear() ) + range - 1;
}
ctrl.activeDate.setFullYear(date);
};
ctrl.refreshView();
}
};
}])
.constant('datepickerPopupConfig', {
datepickerPopup: 'yyyy-MM-dd',
currentText: 'Today',
clearText: 'Clear',
closeText: 'Done',
closeOnDateSelection: true,
appendToBody: false,
showButtonBar: true
})
.directive('datepickerPopup', ['$compile', '$parse', '$document', '$position', 'dateFilter', 'dateParser', 'datepickerPopupConfig',
function ($compile, $parse, $document, $position, dateFilter, dateParser, datepickerPopupConfig) {
return {
restrict: 'EA',
require: 'ngModel',
scope: {
isOpen: '=?',
currentText: '@',
clearText: '@',
closeText: '@',
dateDisabled: '&'
},
link: function(scope, element, attrs, ngModel) {
var dateFormat,
closeOnDateSelection = angular.isDefined(attrs.closeOnDateSelection) ? scope.$parent.$eval(attrs.closeOnDateSelection) : datepickerPopupConfig.closeOnDateSelection,
appendToBody = angular.isDefined(attrs.datepickerAppendToBody) ? scope.$parent.$eval(attrs.datepickerAppendToBody) : datepickerPopupConfig.appendToBody;
scope.showButtonBar = angular.isDefined(attrs.showButtonBar) ? scope.$parent.$eval(attrs.showButtonBar) : datepickerPopupConfig.showButtonBar;
scope.getText = function( key ) {
return scope[key + 'Text'] || datepickerPopupConfig[key + 'Text'];
};
attrs.$observe('datepickerPopup', function(value) {
dateFormat = value || datepickerPopupConfig.datepickerPopup;
ngModel.$render();
});
// popup element used to display calendar
var popupEl = angular.element('<div datepicker-popup-wrap><div datepicker></div></div>');
popupEl.attr({
'ng-model': 'date',
'ng-change': 'dateSelection()'
});
function cameltoDash( string ){
return string.replace(/([A-Z])/g, function($1) { return '-' + $1.toLowerCase(); });
}
// datepicker element
var datepickerEl = angular.element(popupEl.children()[0]);
if ( attrs.datepickerOptions ) {
angular.forEach(scope.$parent.$eval(attrs.datepickerOptions), function( value, option ) {
datepickerEl.attr( cameltoDash(option), value );
});
}
scope.watchData = {};
angular.forEach(['minDate', 'maxDate', 'datepickerMode'], function( key ) {
if ( attrs[key] ) {
var getAttribute = $parse(attrs[key]);
scope.$parent.$watch(getAttribute, function(value){
scope.watchData[key] = value;
});
datepickerEl.attr(cameltoDash(key), 'watchData.' + key);
// Propagate changes from datepicker to outside
if ( key === 'datepickerMode' ) {
var setAttribute = getAttribute.assign;
scope.$watch('watchData.' + key, function(value, oldvalue) {
if ( value !== oldvalue ) {
setAttribute(scope.$parent, value);
}
});
}
}
});
if (attrs.dateDisabled) {
datepickerEl.attr('date-disabled', 'dateDisabled({ date: date, mode: mode })');
}
function parseDate(viewValue) {
if (!viewValue) {
ngModel.$setValidity('date', true);
return null;
} else if (angular.isDate(viewValue) && !isNaN(viewValue)) {
ngModel.$setValidity('date', true);
return viewValue;
} else if (angular.isString(viewValue)) {
var date = dateParser.parse(viewValue, dateFormat) || new Date(viewValue);
if (isNaN(date)) {
ngModel.$setValidity('date', false);
return undefined;
} else {
ngModel.$setValidity('date', true);
return date;
}
} else {
ngModel.$setValidity('date', false);
return undefined;
}
}
ngModel.$parsers.unshift(parseDate);
// Inner change
scope.dateSelection = function(dt) {
if (angular.isDefined(dt)) {
scope.date = dt;
}
ngModel.$setViewValue(scope.date);
ngModel.$render();
if ( closeOnDateSelection ) {
scope.isOpen = false;
element[0].focus();
}
};
element.bind('input change keyup', function() {
scope.$apply(function() {
scope.date = ngModel.$modelValue;
});
});
// Outter change
ngModel.$render = function() {
var date = ngModel.$viewValue ? dateFilter(ngModel.$viewValue, dateFormat) : '';
element.val(date);
scope.date = parseDate( ngModel.$modelValue );
};
var documentClickBind = function(event) {
if (scope.isOpen && event.target !== element[0]) {
scope.$apply(function() {
scope.isOpen = false;
});
}
};
var keydown = function(evt, noApply) {
scope.keydown(evt);
};
element.bind('keydown', keydown);
scope.keydown = function(evt) {
if (evt.which === 27) {
evt.preventDefault();
evt.stopPropagation();
scope.close();
} else if (evt.which === 40 && !scope.isOpen) {
scope.isOpen = true;
}
};
scope.$watch('isOpen', function(value) {
if (value) {
scope.$broadcast('datepicker.focus');
scope.position = appendToBody ? $position.offset(element) : $position.position(element);
scope.position.top = scope.position.top + element.prop('offsetHeight');
$document.bind('click', documentClickBind);
} else {
$document.unbind('click', documentClickBind);
}
});
scope.select = function( date ) {
if (date === 'today') {
var today = new Date();
if (angular.isDate(ngModel.$modelValue)) {
date = new Date(ngModel.$modelValue);
date.setFullYear(today.getFullYear(), today.getMonth(), today.getDate());
} else {
date = new Date(today.setHours(0, 0, 0, 0));
}
}
scope.dateSelection( date );
};
scope.close = function() {
scope.isOpen = false;
element[0].focus();
};
var $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() {
$popup.remove();
element.unbind('keydown', keydown);
$document.unbind('click', documentClickBind);
});
}
};
}])
.directive('datepickerPopupWrap', function() {
return {
restrict:'EA',
replace: true,
transclude: true,
templateUrl: 'template/datepicker/popup.html',
link:function (scope, element, attrs) {
element.bind('click', function(event) {
event.preventDefault();
event.stopPropagation();
});
}
};
});

View file

@ -0,0 +1,31 @@
<div ng-controller="DatepickerDemoCtrl">
<pre>Selected date is: <em>{{dt | date:'fullDate' }}</em></pre>
<h4>Inline</h4>
<div style="display:inline-block; min-height:290px;">
<datepicker ng-model="dt" min-date="minDate" show-weeks="true" class="well well-sm"></datepicker>
</div>
<h4>Popup</h4>
<div class="row">
<div class="col-md-6">
<p class="input-group">
<input type="text" class="form-control" datepicker-popup="{{format}}" ng-model="dt" is-open="opened" min-date="minDate" max-date="'2015-06-22'" datepicker-options="dateOptions" date-disabled="disabled(date, mode)" ng-required="true" close-text="Close" />
<span class="input-group-btn">
<button type="button" class="btn btn-default" ng-click="open($event)"><i class="glyphicon glyphicon-calendar"></i></button>
</span>
</p>
</div>
</div>
<div class="row">
<div class="col-md-6">
<label>Format:</label> <select class="form-control" ng-model="format" ng-options="f for f in formats"><option></option></select>
</div>
</div>
<hr />
<button type="button" class="btn btn-sm btn-info" ng-click="today()">Today</button>
<button type="button" class="btn btn-sm btn-default" ng-click="dt = '2009-08-24'">2009-08-24</button>
<button type="button" class="btn btn-sm btn-danger" ng-click="clear()">Clear</button>
<button type="button" class="btn btn-sm btn-default" ng-click="toggleMin()" tooltip="After today restriction">Min date</button>
</div>

View file

@ -0,0 +1,35 @@
angular.module('ui.bootstrap.demo').controller('DatepickerDemoCtrl', function ($scope) {
$scope.today = function() {
$scope.dt = new Date();
};
$scope.today();
$scope.clear = function () {
$scope.dt = null;
};
// Disable weekend selection
$scope.disabled = function(date, mode) {
return ( mode === 'day' && ( date.getDay() === 0 || date.getDay() === 6 ) );
};
$scope.toggleMin = function() {
$scope.minDate = $scope.minDate ? null : new Date();
};
$scope.toggleMin();
$scope.open = function($event) {
$event.preventDefault();
$event.stopPropagation();
$scope.opened = true;
};
$scope.dateOptions = {
formatYear: 'yy',
startingDay: 1
};
$scope.formats = ['dd-MMMM-yyyy', 'yyyy/MM/dd', 'dd.MM.yyyy', 'shortDate'];
$scope.format = $scope.formats[0];
});

View file

@ -0,0 +1,129 @@
A clean, flexible, and fully customizable date picker.
User can navigate through months and years.
The datepicker shows dates that come from other than the main month being displayed. These other dates are also selectable.
Everything is formatted using the [date filter](http://docs.angularjs.org/api/ng.filter:date) and thus is also localized.
### Datepicker Settings ###
All settings can be provided as attributes in the `datepicker` or globally configured through the `datepickerConfig`.
* `ng-model` <i class="glyphicon glyphicon-eye-open"></i>
:
The date object.
* `datepicker-mode` <i class="glyphicon glyphicon-eye-open"></i>
_(Defaults: 'day')_ :
Current mode of the datepicker _(day|month|year)_. Can be used to initialize datepicker to specific mode.
* `min-date` <i class="glyphicon glyphicon-eye-open"></i>
_(Default: null)_ :
Defines the minimum available date.
* `max-date` <i class="glyphicon glyphicon-eye-open"></i>
_(Default: null)_ :
Defines the maximum available date.
* `date-disabled (date, mode)`
_(Default: null)_ :
An optional expression to disable visible options based on passing date and current mode _(day|month|year)_.
* `show-weeks`
_(Defaults: true)_ :
Whether to display week numbers.
* `starting-day`
_(Defaults: 0)_ :
Starting day of the week from 0-6 (0=Sunday, ..., 6=Saturday).
* `init-date`
:
The initial date view when no model value is not specified.
* `min-mode`
_(Defaults: 'day')_ :
Set a lower limit for mode.
* `max-mode`
_(Defaults: 'year')_ :
Set an upper limit for mode.
* `format-day`
_(Default: 'dd')_ :
Format of day in month.
* `format-month`
_(Default: 'MMMM')_ :
Format of month in year.
* `format-year`
_(Default: 'yyyy')_ :
Format of year in year range.
* `format-day-header`
_(Default: 'EEE')_ :
Format of day in week header.
* `format-day-title`
_(Default: 'MMMM yyyy')_ :
Format of title when selecting day.
* `format-month-title`
_(Default: 'yyyy')_ :
Format of title when selecting month.
* `year-range`
_(Default: 20)_ :
Number of years displayed in year selection.
### Popup Settings ###
Options for datepicker can be passed as JSON using the `datepicker-options` attribute.
Specific settings for the `datepicker-popup`, that can globally configured through the `datepickerPopupConfig`, are:
* `datepicker-popup`
_(Default: 'yyyy-MM-dd')_ :
The format for displayed dates.
* `show-button-bar`
_(Default: true)_ :
Whether to display a button bar underneath the datepicker.
* `current-text`
_(Default: 'Today')_ :
The text to display for the current day button.
* `clear-text`
_(Default: 'Clear')_ :
The text to display for the clear button.
* `close-text`
_(Default: 'Done')_ :
The text to display for the close button.
* `close-on-date-selection`
_(Default: true)_ :
Whether to close calendar when a date is chosen.
* `datepicker-append-to-body`
_(Default: false)_:
Append the datepicker popup element to `body`, rather than inserting after `datepicker-popup`. For global configuration, use `datepickerPopupConfig.appendToBody`.
### Keyboard Support ###
Depending on datepicker's current mode, the date may reffer either to day, month or year. Accordingly, the term view reffers 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.

View file

@ -0,0 +1,51 @@
<div ng-controller="DropdownCtrl">
<!-- Simple dropdown -->
<span class="dropdown" dropdown on-toggle="toggled(open)">
<a href class="dropdown-toggle" dropdown-toggle>
Click me for a dropdown, yo!
</a>
<ul class="dropdown-menu">
<li ng-repeat="choice in items">
<a href>{{choice}}</a>
</li>
</ul>
</span>
<!-- Single button -->
<div class="btn-group" dropdown is-open="status.isopen">
<button type="button" class="btn btn-primary dropdown-toggle" dropdown-toggle ng-disabled="disabled">
Button dropdown <span class="caret"></span>
</button>
<ul class="dropdown-menu" role="menu">
<li><a href="#">Action</a></li>
<li><a href="#">Another action</a></li>
<li><a href="#">Something else here</a></li>
<li class="divider"></li>
<li><a href="#">Separated link</a></li>
</ul>
</div>
<!-- Split button -->
<div class="btn-group" dropdown>
<button type="button" class="btn btn-danger">Action</button>
<button type="button" class="btn btn-danger dropdown-toggle" dropdown-toggle>
<span class="caret"></span>
<span class="sr-only">Split button!</span>
</button>
<ul class="dropdown-menu" role="menu">
<li><a href="#">Action</a></li>
<li><a href="#">Another action</a></li>
<li><a href="#">Something else here</a></li>
<li class="divider"></li>
<li><a href="#">Separated link</a></li>
</ul>
</div>
<hr />
<p>
<button type="button" class="btn btn-default btn-sm" ng-click="toggleDropdown($event)">Toggle button dropdown</button>
<button type="button" class="btn btn-warning btn-sm" ng-click="disabled = !disabled">Enable/Disable</button>
</p>
</div>

View file

@ -0,0 +1,21 @@
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;
};
});

View file

@ -0,0 +1,4 @@
Dropdown is a simple directive which will toggle a dropdown menu on click or programmatically.
You can either use `is-open` to toggle or add inside a `<a dropdown-toggle>` element to toggle it when is clicked.
There is also the `on-toggle(open)` optional expression fired when dropdown changes state.

View file

@ -0,0 +1,161 @@
angular.module('ui.bootstrap.dropdown', [])
.constant('dropdownConfig', {
openClass: 'open'
})
.service('dropdownService', ['$document', function($document) {
var openScope = null;
this.open = function( dropdownScope ) {
if ( !openScope ) {
$document.bind('click', closeDropdown);
$document.bind('keydown', escapeKeyBind);
}
if ( openScope && openScope !== dropdownScope ) {
openScope.isOpen = false;
}
openScope = dropdownScope;
};
this.close = function( dropdownScope ) {
if ( openScope === dropdownScope ) {
openScope = null;
$document.unbind('click', closeDropdown);
$document.unbind('keydown', escapeKeyBind);
}
};
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) { return; }
var toggleElement = openScope.getToggleElement();
if ( evt && toggleElement && toggleElement[0].contains(evt.target) ) {
return;
}
openScope.$apply(function() {
openScope.isOpen = false;
});
};
var escapeKeyBind = function( evt ) {
if ( evt.which === 27 ) {
openScope.focusToggleElement();
closeDropdown();
}
};
}])
.controller('DropdownController', ['$scope', '$attrs', '$parse', 'dropdownConfig', 'dropdownService', '$animate', function($scope, $attrs, $parse, dropdownConfig, dropdownService, $animate) {
var self = this,
scope = $scope.$new(), // create a child scope so we are not polluting original one
openClass = dropdownConfig.openClass,
getIsOpen,
setIsOpen = angular.noop,
toggleInvoker = $attrs.onToggle ? $parse($attrs.onToggle) : angular.noop;
this.init = function( element ) {
self.$element = element;
if ( $attrs.isOpen ) {
getIsOpen = $parse($attrs.isOpen);
setIsOpen = getIsOpen.assign;
$scope.$watch(getIsOpen, function(value) {
scope.isOpen = !!value;
});
}
};
this.toggle = function( open ) {
return scope.isOpen = arguments.length ? !!open : !scope.isOpen;
};
// Allow other directives to watch status
this.isOpen = function() {
return scope.isOpen;
};
scope.getToggleElement = function() {
return self.toggleElement;
};
scope.focusToggleElement = function() {
if ( self.toggleElement ) {
self.toggleElement[0].focus();
}
};
scope.$watch('isOpen', function( isOpen, wasOpen ) {
$animate[isOpen ? 'addClass' : 'removeClass'](self.$element, openClass);
if ( isOpen ) {
scope.focusToggleElement();
dropdownService.open( scope );
} else {
dropdownService.close( scope );
}
setIsOpen($scope, isOpen);
if (angular.isDefined(isOpen) && isOpen !== wasOpen) {
toggleInvoker($scope, { open: !!isOpen });
}
});
$scope.$on('$locationChangeSuccess', function() {
scope.isOpen = false;
});
$scope.$on('$destroy', function() {
scope.$destroy();
});
}])
.directive('dropdown', function() {
return {
controller: 'DropdownController',
link: function(scope, element, attrs, dropdownCtrl) {
dropdownCtrl.init( element );
}
};
})
.directive('dropdownToggle', function() {
return {
require: '?^dropdown',
link: function(scope, element, attrs, dropdownCtrl) {
if ( !dropdownCtrl ) {
return;
}
dropdownCtrl.toggleElement = element;
var toggleDropdown = function(event) {
event.preventDefault();
if ( !element.hasClass('disabled') && !attrs.disabled ) {
scope.$apply(function() {
dropdownCtrl.toggle();
});
}
};
element.bind('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.unbind('click', toggleDropdown);
});
}
};
});

View file

@ -0,0 +1,315 @@
describe('dropdownToggle', function() {
var $compile, $rootScope, $document, element;
beforeEach(module('ui.bootstrap.dropdown'));
beforeEach(inject(function(_$compile_, _$rootScope_, _$document_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
$document = _$document_;
}));
var clickDropdownToggle = function(elm) {
elm = elm || element;
elm.find('a[dropdown-toggle]').click();
};
var triggerKeyDown = function (element, keyCode) {
var e = $.Event('keydown');
e.which = keyCode;
element.trigger(e);
};
var isFocused = function(elm) {
return elm[0] === document.activeElement;
};
describe('basic', function() {
function dropdown() {
return $compile('<li dropdown><a href dropdown-toggle></a><ul><li><a href>Hello</a></li></ul></li>')($rootScope);
}
beforeEach(function() {
element = dropdown();
});
it('should toggle on `a` click', function() {
expect(element.hasClass('open')).toBe(false);
clickDropdownToggle();
expect(element.hasClass('open')).toBe(true);
clickDropdownToggle();
expect(element.hasClass('open')).toBe(false);
});
it('should toggle when an option is clicked', function() {
$document.find('body').append(element);
expect(element.hasClass('open')).toBe(false);
clickDropdownToggle();
expect(element.hasClass('open')).toBe(true);
var optionEl = element.find('ul > li').eq(0).find('a').eq(0);
optionEl.click();
expect(element.hasClass('open')).toBe(false);
element.remove();
});
it('should close on document click', function() {
clickDropdownToggle();
expect(element.hasClass('open')).toBe(true);
$document.click();
expect(element.hasClass('open')).toBe(false);
});
it('should close on escape key & focus toggle element', function() {
$document.find('body').append(element);
clickDropdownToggle();
triggerKeyDown($document, 27);
expect(element.hasClass('open')).toBe(false);
expect(isFocused(element.find('a'))).toBe(true);
element.remove();
});
it('should not close on backspace key', function() {
clickDropdownToggle();
triggerKeyDown($document, 8);
expect(element.hasClass('open')).toBe(true);
});
it('should close on $location change', function() {
clickDropdownToggle();
expect(element.hasClass('open')).toBe(true);
$rootScope.$broadcast('$locationChangeSuccess');
$rootScope.$apply();
expect(element.hasClass('open')).toBe(false);
});
it('should only allow one dropdown to be open at once', function() {
var elm1 = dropdown();
var elm2 = dropdown();
expect(elm1.hasClass('open')).toBe(false);
expect(elm2.hasClass('open')).toBe(false);
clickDropdownToggle( elm1 );
expect(elm1.hasClass('open')).toBe(true);
expect(elm2.hasClass('open')).toBe(false);
clickDropdownToggle( elm2 );
expect(elm1.hasClass('open')).toBe(false);
expect(elm2.hasClass('open')).toBe(true);
});
it('should not toggle if the element has `disabled` class', function() {
var elm = $compile('<li dropdown><a class="disabled" dropdown-toggle></a><ul><li>Hello</li></ul></li>')($rootScope);
clickDropdownToggle( elm );
expect(elm.hasClass('open')).toBe(false);
});
it('should not toggle if the element is disabled', function() {
var elm = $compile('<li dropdown><button disabled="disabled" dropdown-toggle></button><ul><li>Hello</li></ul></li>')($rootScope);
elm.find('button').click();
expect(elm.hasClass('open')).toBe(false);
});
it('should not toggle if the element has `ng-disabled` as true', function() {
$rootScope.isdisabled = true;
var elm = $compile('<li dropdown><div ng-disabled="isdisabled" dropdown-toggle></div><ul><li>Hello</li></ul></li>')($rootScope);
$rootScope.$digest();
elm.find('div').click();
expect(elm.hasClass('open')).toBe(false);
$rootScope.isdisabled = false;
$rootScope.$digest();
elm.find('div').click();
expect(elm.hasClass('open')).toBe(true);
});
it('should unbind events on scope destroy', function() {
var $scope = $rootScope.$new();
var elm = $compile('<li dropdown><button ng-disabled="isdisabled" dropdown-toggle></button><ul><li>Hello</li></ul></li>')($scope);
$scope.$digest();
var buttonEl = elm.find('button');
buttonEl.click();
expect(elm.hasClass('open')).toBe(true);
buttonEl.click();
expect(elm.hasClass('open')).toBe(false);
$scope.$destroy();
buttonEl.click();
expect(elm.hasClass('open')).toBe(false);
});
// issue 270
it('executes other document click events normally', function() {
var checkboxEl = $compile('<input type="checkbox" ng-click="clicked = true" />')($rootScope);
$rootScope.$digest();
expect(element.hasClass('open')).toBe(false);
expect($rootScope.clicked).toBeFalsy();
clickDropdownToggle();
expect(element.hasClass('open')).toBe(true);
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');
});
});
describe('integration with $location URL rewriting', function() {
function dropdown() {
// Simulate URL rewriting behavior
$document.on('click', 'a[href="#something"]', function () {
$rootScope.$broadcast('$locationChangeSuccess');
$rootScope.$apply();
});
return $compile('<li dropdown><a href dropdown-toggle></a>' +
'<ul><li><a href="#something">Hello</a></li></ul></li>')($rootScope);
}
beforeEach(function() {
element = dropdown();
});
it('should close without errors on $location change', function() {
$document.find('body').append(element);
clickDropdownToggle();
expect(element.hasClass('open')).toBe(true);
var optionEl = element.find('ul > li').eq(0).find('a').eq(0);
optionEl.click();
expect(element.hasClass('open')).toBe(false);
});
});
describe('without trigger', function() {
beforeEach(function() {
$rootScope.isopen = true;
element = $compile('<li dropdown is-open="isopen"><ul><li>Hello</li></ul></li>')($rootScope);
$rootScope.$digest();
});
it('should be open initially', function() {
expect(element.hasClass('open')).toBe(true);
});
it('should toggle when `is-open` changes', function() {
$rootScope.isopen = false;
$rootScope.$digest();
expect(element.hasClass('open')).toBe(false);
});
});
describe('`is-open`', function() {
beforeEach(function() {
$rootScope.isopen = true;
element = $compile('<li dropdown is-open="isopen"><a href dropdown-toggle></a><ul><li>Hello</li></ul></li>')($rootScope);
$rootScope.$digest();
});
it('should be open initially', function() {
expect(element.hasClass('open')).toBe(true);
});
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.hasClass('open')).toBe(false);
});
it('focus toggle element when opening', function() {
$document.find('body').append(element);
clickDropdownToggle();
$rootScope.isopen = false;
$rootScope.$digest();
expect(isFocused(element.find('a'))).toBe(false);
$rootScope.isopen = true;
$rootScope.$digest();
expect(isFocused(element.find('a'))).toBe(true);
element.remove();
});
});
describe('`on-toggle`', function() {
beforeEach(function() {
$rootScope.toggleHandler = jasmine.createSpy('toggleHandler');
$rootScope.isopen = false;
element = $compile('<li dropdown on-toggle="toggleHandler(open)" is-open="isopen"><a dropdown-toggle></a><ul><li>Hello</li></ul></li>')($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();
expect($rootScope.toggleHandler).toHaveBeenCalledWith(true);
clickDropdownToggle();
expect($rootScope.toggleHandler).toHaveBeenCalledWith(false);
});
});
describe('`on-toggle` with initially open', function() {
beforeEach(function() {
$rootScope.toggleHandler = jasmine.createSpy('toggleHandler');
$rootScope.isopen = true;
element = $compile('<li dropdown on-toggle="toggleHandler(open)" is-open="isopen"><a dropdown-toggle></a><ul><li>Hello</li></ul></li>')($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();
expect($rootScope.toggleHandler).toHaveBeenCalledWith(false);
$rootScope.isopen = true;
$rootScope.$digest();
expect($rootScope.toggleHandler).toHaveBeenCalledWith(true);
});
});
describe('`on-toggle` without is-open', function() {
beforeEach(function() {
$rootScope.toggleHandler = jasmine.createSpy('toggleHandler');
element = $compile('<li dropdown on-toggle="toggleHandler(open)"><a dropdown-toggle></a><ul><li>Hello</li></ul></li>')($rootScope);
$rootScope.$digest();
});
it('should not have been called initially', function() {
expect($rootScope.toggleHandler).not.toHaveBeenCalled();
});
it('should call it when clicked', function() {
clickDropdownToggle();
expect($rootScope.toggleHandler).toHaveBeenCalledWith(true);
clickDropdownToggle();
expect($rootScope.toggleHandler).toHaveBeenCalledWith(false);
});
});
});

View file

@ -0,0 +1,24 @@
<div ng-controller="ModalDemoCtrl">
<script type="text/ng-template" id="myModalContent.html">
<div class="modal-header">
<h3 class="modal-title">I'm a modal!</h3>
</div>
<div class="modal-body">
<ul>
<li ng-repeat="item in items">
<a ng-click="selected.item = item">{{ item }}</a>
</li>
</ul>
Selected: <b>{{ selected.item }}</b>
</div>
<div class="modal-footer">
<button class="btn btn-primary" ng-click="ok()">OK</button>
<button class="btn btn-warning" ng-click="cancel()">Cancel</button>
</div>
</script>
<button class="btn btn-default" ng-click="open()">Open me!</button>
<button class="btn btn-default" ng-click="open('lg')">Large modal</button>
<button class="btn btn-default" ng-click="open('sm')">Small modal</button>
<div ng-show="selected">Selection from a modal: {{ selected }}</div>
</div>

View file

@ -0,0 +1,43 @@
angular.module('ui.bootstrap.demo').controller('ModalDemoCtrl', function ($scope, $modal, $log) {
$scope.items = ['item1', 'item2', 'item3'];
$scope.open = function (size) {
var modalInstance = $modal.open({
templateUrl: 'myModalContent.html',
controller: 'ModalInstanceCtrl',
size: size,
resolve: {
items: function () {
return $scope.items;
}
}
});
modalInstance.result.then(function (selectedItem) {
$scope.selected = selectedItem;
}, function () {
$log.info('Modal dismissed at: ' + new Date());
});
};
});
// Please note that $modalInstance represents a modal window (instance) dependency.
// It is not the same as the $modal service used above.
angular.module('ui.bootstrap.demo').controller('ModalInstanceCtrl', function ($scope, $modalInstance, items) {
$scope.items = items;
$scope.selected = {
item: $scope.items[0]
};
$scope.ok = function () {
$modalInstance.close($scope.selected.item);
};
$scope.cancel = function () {
$modalInstance.dismiss('cancel');
};
});

View file

@ -0,0 +1,31 @@
`$modal` is a service to quickly create AngularJS-powered modal windows.
Creating custom modals is straightforward: create a partial view, its controller and reference them when using the service.
The `$modal` service has only one method: `open(options)` where available options are like follows:
* `templateUrl` - a path to a template representing modal's content
* `template` - inline template representing the modal's content
* `scope` - a scope instance to be used for the modal's content (actually the `$modal` service is going to create a child scope of a provided scope). Defaults to `$rootScope`
* `controller` - a controller for a modal instance - it can initialize scope used by modal. Accepts the "controller-as" syntax in the form 'SomeCtrl as myctrl'; can be injected with `$modalInstance`
* `controllerAs` - an alternative to the controller-as syntax, matching the API of directive definitions. Requires the `controller` option to be provided as well
* `resolve` - members that will be resolved and passed to the controller as locals; it is equivalent of the `resolve` property for AngularJS routes
* `backdrop` - controls presence of a backdrop. Allowed values: true (default), false (no backdrop), `'static'` - backdrop is present but modal window is not closed when clicking outside of the modal window.
* `keyboard` - indicates whether the dialog should be closable by hitting the ESC key, defaults to true
* `backdropClass` - additional CSS class(es) to be added to a modal backdrop template
* `windowClass` - additional CSS class(es) to be added to a modal window template
* `windowTemplateUrl` - a path to a template overriding modal's window template
* `size` - optional size of modal window. Allowed values: `'sm'` (small) or `'lg'` (large). Requires Bootstrap 3.1.0 or later
The `open` method returns a modal instance, an object with the following properties:
* `close(result)` - a method that can be used to close a modal, passing a result
* `dismiss(reason)` - a method that can be used to dismiss a modal, passing a reason
* `result` - a promise that is resolved when a modal is closed and rejected when a modal is dismissed
* `opened` - a promise that is resolved when a modal gets opened after downloading content's template and resolving all variables
In addition the scope associated with modal's content is augmented with 2 methods:
* `$close(result)`
* `$dismiss(reason)`
Those methods make it easy to close a modal window without a need to create a dedicated controller.

View file

@ -0,0 +1,415 @@
angular.module('ui.bootstrap.modal', ['ui.bootstrap.transition'])
/**
* 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.splice(stack.length - 1, 1)[0];
},
length: function () {
return stack.length;
}
};
}
};
})
/**
* A helper directive for the $modal service. It creates a backdrop element.
*/
.directive('modalBackdrop', ['$timeout', function ($timeout) {
return {
restrict: 'EA',
replace: true,
templateUrl: 'template/modal/backdrop.html',
link: function (scope, element, attrs) {
scope.backdropClass = attrs.backdropClass || '';
scope.animate = false;
//trigger CSS transitions
$timeout(function () {
scope.animate = true;
});
}
};
}])
.directive('modalWindow', ['$modalStack', '$timeout', function ($modalStack, $timeout) {
return {
restrict: 'EA',
scope: {
index: '@',
animate: '='
},
replace: true,
transclude: true,
templateUrl: function(tElement, tAttrs) {
return tAttrs.templateUrl || 'template/modal/window.html';
},
link: function (scope, element, attrs) {
element.addClass(attrs.windowClass || '');
scope.size = attrs.size;
$timeout(function () {
// trigger CSS transitions
scope.animate = true;
/**
* 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 (!element[0].querySelectorAll('[autofocus]').length) {
element[0].focus();
}
});
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');
}
};
}
};
}])
.directive('modalTransclude', function () {
return {
link: function($scope, $element, $attrs, controller, $transclude) {
$transclude($scope.$parent, function(clone) {
$element.empty();
$element.append(clone);
});
}
};
})
.factory('$modalStack', ['$transition', '$timeout', '$document', '$compile', '$rootScope', '$$stackedMap',
function ($transition, $timeout, $document, $compile, $rootScope, $$stackedMap) {
var OPENED_MODAL_CLASS = 'modal-open';
var backdropDomEl, backdropScope;
var openedWindows = $$stackedMap.createNew();
var $modalStack = {};
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;
}
}
return topBackdropIndex;
}
$rootScope.$watch(backdropIndex, function(newBackdropIndex){
if (backdropScope) {
backdropScope.index = newBackdropIndex;
}
});
function removeModalWindow(modalInstance) {
var body = $document.find('body').eq(0);
var modalWindow = openedWindows.get(modalInstance).value;
//clean up the stack
openedWindows.remove(modalInstance);
//remove window DOM element
removeAfterAnimate(modalWindow.modalDomEl, modalWindow.modalScope, 300, function() {
modalWindow.modalScope.$destroy();
body.toggleClass(OPENED_MODAL_CLASS, openedWindows.length() > 0);
checkRemoveBackdrop();
});
}
function checkRemoveBackdrop() {
//remove backdrop if no longer needed
if (backdropDomEl && backdropIndex() == -1) {
var backdropScopeRef = backdropScope;
removeAfterAnimate(backdropDomEl, backdropScope, 150, function () {
backdropScopeRef.$destroy();
backdropScopeRef = null;
});
backdropDomEl = undefined;
backdropScope = undefined;
}
}
function removeAfterAnimate(domEl, scope, emulateTime, done) {
// Closing animation
scope.animate = false;
var transitionEndEventName = $transition.transitionEndEventName;
if (transitionEndEventName) {
// transition out
var timeout = $timeout(afterAnimating, emulateTime);
domEl.bind(transitionEndEventName, function () {
$timeout.cancel(timeout);
afterAnimating();
scope.$apply();
});
} else {
// Ensure this call is async
$timeout(afterAnimating);
}
function afterAnimating() {
if (afterAnimating.done) {
return;
}
afterAnimating.done = true;
domEl.remove();
if (done) {
done();
}
}
}
$document.bind('keydown', function (evt) {
var modal;
if (evt.which === 27) {
modal = openedWindows.top();
if (modal && modal.value.keyboard) {
evt.preventDefault();
$rootScope.$apply(function () {
$modalStack.dismiss(modal.key, 'escape key press');
});
}
}
});
$modalStack.open = function (modalInstance, modal) {
openedWindows.add(modalInstance, {
deferred: modal.deferred,
modalScope: modal.scope,
backdrop: modal.backdrop,
keyboard: modal.keyboard
});
var body = $document.find('body').eq(0),
currBackdropIndex = backdropIndex();
if (currBackdropIndex >= 0 && !backdropDomEl) {
backdropScope = $rootScope.$new(true);
backdropScope.index = currBackdropIndex;
var angularBackgroundDomEl = angular.element('<div modal-backdrop></div>');
angularBackgroundDomEl.attr('backdrop-class', modal.backdropClass);
backdropDomEl = $compile(angularBackgroundDomEl)(backdropScope);
body.append(backdropDomEl);
}
var angularDomEl = angular.element('<div modal-window></div>');
angularDomEl.attr({
'template-url': modal.windowTemplateUrl,
'window-class': modal.windowClass,
'size': modal.size,
'index': openedWindows.length() - 1,
'animate': 'animate'
}).html(modal.content);
var modalDomEl = $compile(angularDomEl)(modal.scope);
openedWindows.top().value.modalDomEl = modalDomEl;
body.append(modalDomEl);
body.addClass(OPENED_MODAL_CLASS);
};
$modalStack.close = function (modalInstance, result) {
var modalWindow = openedWindows.get(modalInstance);
if (modalWindow) {
modalWindow.value.deferred.resolve(result);
removeModalWindow(modalInstance);
}
};
$modalStack.dismiss = function (modalInstance, reason) {
var modalWindow = openedWindows.get(modalInstance);
if (modalWindow) {
modalWindow.value.deferred.reject(reason);
removeModalWindow(modalInstance);
}
};
$modalStack.dismissAll = function (reason) {
var topModal = this.getTop();
while (topModal) {
this.dismiss(topModal.key, reason);
topModal = this.getTop();
}
};
$modalStack.getTop = function () {
return openedWindows.top();
};
return $modalStack;
}])
.provider('$modal', function () {
var $modalProvider = {
options: {
backdrop: true, //can be also false or 'static'
keyboard: true
},
$get: ['$injector', '$rootScope', '$q', '$http', '$templateCache', '$controller', '$modalStack',
function ($injector, $rootScope, $q, $http, $templateCache, $controller, $modalStack) {
var $modal = {};
function getTemplatePromise(options) {
return options.template ? $q.when(options.template) :
$http.get(angular.isFunction(options.templateUrl) ? (options.templateUrl)() : options.templateUrl,
{cache: $templateCache}).then(function (result) {
return result.data;
});
}
function getResolvePromises(resolves) {
var promisesArr = [];
angular.forEach(resolves, function (value) {
if (angular.isFunction(value) || angular.isArray(value)) {
promisesArr.push($q.when($injector.invoke(value)));
}
});
return promisesArr;
}
$modal.open = function (modalOptions) {
var modalResultDeferred = $q.defer();
var modalOpenedDeferred = $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,
close: function (result) {
$modalStack.close(modalInstance, result);
},
dismiss: function (reason) {
$modalStack.dismiss(modalInstance, reason);
}
};
//merge and clean up options
modalOptions = angular.extend({}, $modalProvider.options, modalOptions);
modalOptions.resolve = modalOptions.resolve || {};
//verify options
if (!modalOptions.template && !modalOptions.templateUrl) {
throw new Error('One of template or templateUrl options is required.');
}
var templateAndResolvePromise =
$q.all([getTemplatePromise(modalOptions)].concat(getResolvePromises(modalOptions.resolve)));
templateAndResolvePromise.then(function resolveSuccess(tplAndVars) {
var modalScope = (modalOptions.scope || $rootScope).$new();
modalScope.$close = modalInstance.close;
modalScope.$dismiss = modalInstance.dismiss;
var ctrlInstance, ctrlLocals = {};
var resolveIter = 1;
//controllers
if (modalOptions.controller) {
ctrlLocals.$scope = modalScope;
ctrlLocals.$modalInstance = modalInstance;
angular.forEach(modalOptions.resolve, function (value, key) {
ctrlLocals[key] = tplAndVars[resolveIter++];
});
ctrlInstance = $controller(modalOptions.controller, ctrlLocals);
if (modalOptions.controllerAs) {
modalScope[modalOptions.controllerAs] = ctrlInstance;
}
}
$modalStack.open(modalInstance, {
scope: modalScope,
deferred: modalResultDeferred,
content: tplAndVars[0],
backdrop: modalOptions.backdrop,
keyboard: modalOptions.keyboard,
backdropClass: modalOptions.backdropClass,
windowClass: modalOptions.windowClass,
windowTemplateUrl: modalOptions.windowTemplateUrl,
size: modalOptions.size
});
}, function resolveError(reason) {
modalResultDeferred.reject(reason);
});
templateAndResolvePromise.then(function () {
modalOpenedDeferred.resolve(true);
}, function () {
modalOpenedDeferred.reject(false);
});
return modalInstance;
};
return $modal;
}]
};
return $modalProvider;
});

View file

@ -0,0 +1,610 @@
describe('$modal', function () {
var $controllerProvider, $rootScope, $document, $compile, $templateCache, $timeout, $q;
var $modal, $modalProvider;
var triggerKeyDown = function (element, keyCode) {
var e = $.Event('keydown');
e.which = keyCode;
element.trigger(e);
};
var waitForBackdropAnimation = function () {
inject(function ($transition) {
if ($transition.transitionEndEventName) {
$timeout.flush();
}
});
};
beforeEach(module('ui.bootstrap.modal'));
beforeEach(module('template/modal/backdrop.html'));
beforeEach(module('template/modal/window.html'));
beforeEach(module(function(_$controllerProvider_, _$modalProvider_){
$controllerProvider = _$controllerProvider_;
$modalProvider = _$modalProvider_;
}));
beforeEach(inject(function (_$rootScope_, _$document_, _$compile_, _$templateCache_, _$timeout_, _$q_, _$modal_) {
$rootScope = _$rootScope_;
$document = _$document_;
$compile = _$compile_;
$templateCache = _$templateCache_;
$timeout = _$timeout_;
$q = _$q_;
$modal = _$modal_;
}));
beforeEach(function () {
this.addMatchers({
toBeResolvedWith: function(value) {
var resolved;
this.message = function() {
return 'Expected "' + angular.mock.dump(this.actual) + '" to be resolved with "' + value + '".';
};
this.actual.then(function(result){
resolved = result;
});
$rootScope.$digest();
return resolved === value;
},
toBeRejectedWith: function(value) {
var rejected;
this.message = function() {
return 'Expected "' + angular.mock.dump(this.actual) + '" to be rejected with "' + value + '".';
};
this.actual.then(angular.noop, function(reason){
rejected = reason;
});
$rootScope.$digest();
return rejected === value;
},
toHaveModalOpenWithContent: function(content, selector) {
var contentToCompare, modalDomEls = this.actual.find('body > div.modal > div.modal-dialog > div.modal-content');
this.message = function() {
return '"Expected "' + angular.mock.dump(modalDomEls) + '" to be open with "' + content + '".';
};
contentToCompare = selector ? modalDomEls.find(selector) : modalDomEls;
return modalDomEls.css('display') === 'block' && contentToCompare.html() == content;
},
toHaveModalsOpen: function(noOfModals) {
var modalDomEls = this.actual.find('body > div.modal');
return modalDomEls.length === noOfModals;
},
toHaveBackdrop: function() {
var backdropDomEls = this.actual.find('body > div.modal-backdrop');
this.message = function() {
return 'Expected "' + angular.mock.dump(backdropDomEls) + '" to be a backdrop element".';
};
return backdropDomEls.length === 1;
}
});
});
afterEach(function () {
var body = $document.find('body');
body.find('div.modal').remove();
body.find('div.modal-backdrop').remove();
body.removeClass('modal-open');
});
function open(modalOptions) {
var modal = $modal.open(modalOptions);
$rootScope.$digest();
return modal;
}
function close(modal, result) {
modal.close(result);
$timeout.flush();
$rootScope.$digest();
}
function dismiss(modal, reason) {
modal.dismiss(reason);
$timeout.flush();
$rootScope.$digest();
}
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: '<div>Content</div>'});
expect($document).toHaveModalsOpen(1);
expect($document).toHaveModalOpenWithContent('Content', 'div');
expect($document).toHaveBackdrop();
dismiss(modal, 'closing in test');
expect($document).toHaveModalsOpen(0);
waitForBackdropAnimation();
expect($document).not.toHaveBackdrop();
});
it('should not throw an exception on a second dismiss', function () {
var modal = open({template: '<div>Content</div>'});
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');
});
it('should not throw an exception on a second close', function () {
var modal = open({template: '<div>Content</div>'});
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');
});
it('should open a modal from templateUrl', function () {
$templateCache.put('content.html', '<div>URL Content</div>');
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);
waitForBackdropAnimation();
expect($document).not.toHaveBackdrop();
});
it('should support closing on ESC', function () {
var modal = open({template: '<div>Content</div>'});
expect($document).toHaveModalsOpen(1);
triggerKeyDown($document, 27);
$timeout.flush();
$rootScope.$digest();
expect($document).toHaveModalsOpen(0);
});
it('should support closing on backdrop click', function () {
var modal = open({template: '<div>Content</div>'});
expect($document).toHaveModalsOpen(1);
$document.find('body > div.modal').click();
$timeout.flush();
$rootScope.$digest();
expect($document).toHaveModalsOpen(0);
});
it('should resolve returned promise on close', function () {
var modal = open({template: '<div>Content</div>'});
close(modal, 'closed ok');
expect(modal.result).toBeResolvedWith('closed ok');
});
it('should reject returned promise on dismiss', function () {
var modal = open({template: '<div>Content</div>'});
dismiss(modal, 'esc');
expect(modal.result).toBeRejectedWith('esc');
});
it('should expose a promise linked to the templateUrl / resolve promises', function () {
var modal = open({template: '<div>Content</div>', 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: '<div>Content</div>', resolve: {
ok: function() {return $q.reject('ko');}
}}
);
expect(modal.opened).toBeRejectedWith(false);
});
});
describe('default options can be changed in a provider', function () {
it('should allow overriding default options in a provider', function () {
$modalProvider.options.backdrop = false;
var modal = open({template: '<div>Content</div>'});
expect($document).toHaveModalOpenWithContent('Content', 'div');
expect($document).not.toHaveBackdrop();
});
it('should accept new objects with default options in a provider', function () {
$modalProvider.options = {
backdrop: false
};
var modal = open({template: '<div>Content</div>'});
expect($document).toHaveModalOpenWithContent('Content', 'div');
expect($document).not.toHaveBackdrop();
});
});
describe('option by option', function () {
describe('template and templateUrl', function () {
it('should throw an error if none of template and templateUrl are provided', function () {
expect(function(){
var modal = open({});
}).toThrow(new Error('One of template or templateUrl options is required.'));
});
it('should not fail if a templateUrl contains leading / trailing white spaces', function () {
$templateCache.put('whitespace.html', ' <div>Whitespaces</div> ');
open({templateUrl: 'whitespace.html'});
expect($document).toHaveModalOpenWithContent('Whitespaces', 'div');
});
it('should accept template as a function', function () {
open({template: function() {
return '<div>From a function</div>';
}});
expect($document).toHaveModalOpenWithContent('From a function', 'div');
});
it('should not fail if a templateUrl as a function', function () {
$templateCache.put('whitespace.html', ' <div>Whitespaces</div> ');
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, $modalInstance) {
$scope.fromCtrl = 'Content from ctrl';
$scope.isModalInstance = angular.isObject($modalInstance) && angular.isFunction($modalInstance.close);
};
open({template: '<div>{{fromCtrl}} {{isModalInstance}}</div>', controller: TestCtrl});
expect($document).toHaveModalOpenWithContent('Content from ctrl true', 'div');
});
it('should accept controllerAs alias', function () {
$controllerProvider.register('TestCtrl', function($modalInstance) {
this.fromCtrl = 'Content from ctrl';
this.isModalInstance = angular.isObject($modalInstance) && angular.isFunction($modalInstance.close);
});
open({template: '<div>{{test.fromCtrl}} {{test.isModalInstance}}</div>', 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($modalInstance) {
this.fromCtrl = 'Content from ctrl';
this.isModalInstance = angular.isObject($modalInstance) && angular.isFunction($modalInstance.close);
});
open({template: '<div>{{test.fromCtrl}} {{test.isModalInstance}}</div>', controller: 'TestCtrl', controllerAs: 'test'});
expect($document).toHaveModalOpenWithContent('Content from ctrl true', 'div');
});
it('should allow defining in-place controller-as controllers', function () {
open({template: '<div>{{test.fromCtrl}} {{test.isModalInstance}}</div>', controller: function($modalInstance) {
this.fromCtrl = 'Content from ctrl';
this.isModalInstance = angular.isObject($modalInstance) && angular.isFunction($modalInstance.close);
}, controllerAs: 'test'});
expect($document).toHaveModalOpenWithContent('Content from ctrl true', 'div');
});
});
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('<div>{{value}}</div>', {
value: function () {
return 'Content from resolve';
}
}));
expect($document).toHaveModalOpenWithContent('Content from resolve', 'div');
});
it('should delay showing modal if one of the resolves is a promise', function () {
open(modalDefinition('<div>{{value}}</div>', {
value: function () {
return $timeout(function(){ return 'Promise'; }, 100);
}
}));
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('<div>{{value}}</div>', {
value: function () {
return deferred.promise;
}
}));
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('<div>{{value.id}}</div>', {
value: ['$locale', function (e) {
return e;
}]
}));
expect($document).toHaveModalOpenWithContent('en-us', 'div');
});
//TODO: resolves with dependency injection - do we want to support them?
});
describe('scope', function () {
it('should use custom scope if provided', function () {
var $scope = $rootScope.$new();
$scope.fromScope = 'Content from custom scope';
open({
template: '<div>{{fromScope}}</div>',
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: '<div>{{fromScope}}</div>'
});
expect($document).toHaveModalOpenWithContent('Content from root scope', 'div');
});
});
describe('keyboard', function () {
it('should not close modals if keyboard option is set to false', function () {
open({
template: '<div>No keyboard</div>',
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: '<div>No backdrop</div>',
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: '<div>Static backdrop</div>',
backdrop: 'static'
});
$document.find('body > div.modal-backdrop').click();
$rootScope.$digest();
expect($document).toHaveModalOpenWithContent('Static backdrop', 'div');
expect($document).toHaveBackdrop();
});
it('should animate backdrop on each modal opening', function () {
var modal = open({ template: '<div>With backdrop</div>' });
var backdropEl = $document.find('body > div.modal-backdrop');
expect(backdropEl).not.toHaveClass('in');
$timeout.flush();
expect(backdropEl).toHaveClass('in');
dismiss(modal);
waitForBackdropAnimation();
modal = open({ template: '<div>With backdrop</div>' });
backdropEl = $document.find('body > div.modal-backdrop');
expect(backdropEl).not.toHaveClass('in');
});
describe('custom backdrop classes', function () {
it('should support additional backdrop class as string', function () {
open({
template: '<div>With custom backdrop class</div>',
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: '<div>With custom window class</div>',
windowClass: 'additional'
});
expect($document.find('div.modal')).toHaveClass('additional');
});
});
describe('size', function () {
it('should support creating small modal dialogs', function () {
open({
template: '<div>Small modal dialog</div>',
size: 'sm'
});
expect($document.find('div.modal-dialog')).toHaveClass('modal-sm');
});
it('should support creating large modal dialogs', function () {
open({
template: '<div>Large modal dialog</div>',
size: 'lg'
});
expect($document.find('div.modal-dialog')).toHaveClass('modal-lg');
});
});
});
describe('multiple modals', function () {
it('it should allow opening of multiple modals', function () {
var modal1 = open({template: '<div>Modal1</div>'});
var modal2 = open({template: '<div>Modal2</div>'});
expect($document).toHaveModalsOpen(2);
dismiss(modal2);
expect($document).toHaveModalsOpen(1);
expect($document).toHaveModalOpenWithContent('Modal1', 'div');
dismiss(modal1);
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: '<div>Modal1</div>'});
var modal2 = open({template: '<div>Modal2</div>', 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: '<div>Modal1</div>'});
var modal2 = open({template: '<div>Modal2</div>', backdrop: false});
$document.find('body > div.modal-backdrop').click();
$rootScope.$digest();
expect($document).toHaveModalsOpen(2);
});
it('multiple modals should not interfere with default options', function () {
var modal1 = open({template: '<div>Modal1</div>', backdrop: false});
var modal2 = open({template: '<div>Modal2</div>'});
$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: '<div>Content1</div>'});
expect(body).toHaveClass('modal-open');
var modal2 = open({template: '<div>Content1</div>'});
expect(body).toHaveClass('modal-open');
dismiss(modal1);
expect(body).toHaveClass('modal-open');
dismiss(modal2);
expect(body).not.toHaveClass('modal-open');
});
});
});

View file

@ -0,0 +1,36 @@
describe('modal window', function () {
var $rootScope, $compile;
beforeEach(module('ui.bootstrap.modal'));
beforeEach(module('template/modal/window.html'));
beforeEach(inject(function (_$rootScope_, _$compile_) {
$rootScope = _$rootScope_;
$compile = _$compile_;
}));
it('should not use transclusion scope for modals content - issue 2110', function () {
$compile('<div modal-window><span ng-init="foo=true"></span></div>')($rootScope);
$rootScope.$digest();
expect($rootScope.foo).toBeTruthy();
});
it('should support custom CSS classes as string', function () {
var windowEl = $compile('<div modal-window window-class="test foo">content</div>')($rootScope);
$rootScope.$digest();
expect(windowEl).toHaveClass('test');
expect(windowEl).toHaveClass('foo');
});
it('should support custom template url', inject(function($templateCache) {
$templateCache.put('window.html', '<div class="mywindow" ng-transclude></div>');
var windowEl = $compile('<div modal-window template-url="window.html" window-class="test">content</div>')($rootScope);
$rootScope.$digest();
expect(windowEl).toHaveClass('mywindow');
expect(windowEl).toHaveClass('test');
}));
});

View file

@ -0,0 +1,57 @@
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();
});
});

View file

@ -0,0 +1,19 @@
<div ng-controller="PaginationDemoCtrl">
<h4>Default</h4>
<pagination total-items="totalItems" ng-model="currentPage" ng-change="pageChanged()"></pagination>
<pagination boundary-links="true" total-items="totalItems" ng-model="currentPage" class="pagination-sm" previous-text="&lsaquo;" next-text="&rsaquo;" first-text="&laquo;" last-text="&raquo;"></pagination>
<pagination direction-links="false" boundary-links="true" total-items="totalItems" ng-model="currentPage"></pagination>
<pagination direction-links="false" total-items="totalItems" ng-model="currentPage" num-pages="smallnumPages"></pagination>
<pre>The selected page no: {{currentPage}}</pre>
<button class="btn btn-info" ng-click="setPage(3)">Set current page to: 3</button>
<hr />
<h4>Pager</h4>
<pager total-items="totalItems" ng-model="currentPage"></pager>
<hr />
<h4>Limit the maximum visible buttons</h4>
<pagination total-items="bigTotalItems" ng-model="bigCurrentPage" max-size="maxSize" class="pagination-sm" boundary-links="true"></pagination>
<pagination total-items="bigTotalItems" ng-model="bigCurrentPage" max-size="maxSize" class="pagination-sm" boundary-links="true" rotate="false" num-pages="numPages"></pagination>
<pre>Page: {{bigCurrentPage}} / {{numPages}}</pre>
</div>

Some files were not shown because too many files have changed in this diff Show more