diff --git a/.eslintrc b/.eslintrc
new file mode 100644
index 0000000000..85407969a3
--- /dev/null
+++ b/.eslintrc
@@ -0,0 +1,76 @@
+{
+ "env": {
+ "browser": true,
+ "es6": true
+ },
+ "rules": {
+ "comma-dangle": 2,
+ "no-cond-assign": 2,
+ "no-control-regex": 2,
+ "no-debugger": 2,
+ "no-dupe-args": 2,
+ "no-dupe-keys": 2,
+ "no-duplicate-case": 2,
+ "no-empty-character-class": 2,
+ "no-empty": 2,
+ "no-ex-assign": 2,
+ "no-extra-boolean-cast": 2,
+ "no-extra-parens": 2,
+ "no-extra-semi": 2,
+ "no-func-assign": 2,
+ "no-inner-declarations": 2,
+ "no-invalid-regexp": 2,
+ "no-negated-in-lhs": 2,
+ "no-regex-spaces": 2,
+ "no-sparse-arrays": 2,
+ "no-unexpected-multiline": 2,
+ "no-unreachable": 2,
+ "valid-typeof": 2,
+ "accessor-pairs": 2,
+ "block-scoped-var": 2,
+ "complexity": 2,
+ "curly": 2,
+ "dot-notation": [2, {"allowKeywords": false}],
+ "eqeqeq": 2,
+ "guard-for-in": 2,
+ "semi": 2,
+ "no-alert": 2,
+ "no-caller": 2,
+ "no-case-declarations": 2,
+ "no-div-regex": 2,
+ "no-else-return": 2,
+ "no-empty-label": 2,
+ "no-empty-pattern": 2,
+ "no-eq-null": 2,
+ "no-eval": 2,
+ "no-extend-native": 2,
+ "no-extra-bind": 2,
+ "no-floating-decimal": 2,
+ "no-implied-eval": 2,
+ "no-invalid-this": 2,
+ "no-iterator": 2,
+ "no-lone-blocks": 2,
+ "no-loop-func": 2,
+ "no-multi-spaces": 2,
+ "no-native-reassign": 2,
+ "no-new-func": 2,
+ "no-new-wrappers": 2,
+ "no-new": 2,
+ "no-octal-escape": 2,
+ "no-octal": 2,
+ "no-proto": 2,
+ "no-redeclare": 2,
+ "no-script-url": 2,
+ "no-self-compare": 2,
+ "no-sequences": 2,
+ "no-throw-literal": 2,
+ "no-unused-expressions": 2,
+ "no-useless-call": 2,
+ "no-useless-concat": 2,
+ "no-void": 2,
+ "no-with": 2,
+ "radix": 2,
+ "wrap-iife": 2,
+ "yoda": 2
+ }
+}
diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
new file mode 100644
index 0000000000..6b74877384
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE.md
@@ -0,0 +1,5 @@
+# PLEASE READ
+
+As per the [README](https://github.com/angular-ui/bootstrap/blob/master/README.md), this project is no longer being maintained. Any issues entered will remain uninvestigated and unresolved.
+
+We thank you for your contributions over the years. This library would not have been successful without them.
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000000..676cdb40f9
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,5 @@
+# PLEASE READ
+
+As per the [README](https://github.com/angular-ui/bootstrap/blob/master/README.md), this project is no longer being maintained. Any PRs entered will not be reviewed or merged and will remain open.
+
+We thank you for your contributions over the years. This library would not have been successful without them.
diff --git a/.gitignore b/.gitignore
index d106a75394..6716155984 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,13 +9,14 @@ lib-cov
*.swp
*.swo
.DS_Store
+.idea
pids
logs
results
dist
# test coverage files
-coverage/
+.coverage/
node_modules
npm-debug.log
diff --git a/.npmignore b/.npmignore
new file mode 100644
index 0000000000..925a2cd919
--- /dev/null
+++ b/.npmignore
@@ -0,0 +1,38 @@
+lib-cov
+*.seed
+*.log
+*.csv
+*.dat
+*.out
+*.pid
+*.gz
+*.swp
+*.swo
+.DS_Store
+
+pids
+logs
+results
+# test coverage files
+.coverage/
+
+node_modules
+npm-debug.log
+
+.git
+docs
+misc
+.editorconfig
+.gitattributes
+.gitignore
+.jshintrc
+.travis.yml
+CONTRIBUTING.md
+Gruntfile.js
+karma.conf.js
+ROADMAP.md
+
+dist/assets
+dist/index.html
+dist/versions-mapping.json
+dist/*-SNAPSHOT*
diff --git a/.travis.yml b/.travis.yml
index cccd617de3..e69e4c76f0 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,11 +1,20 @@
- language: node_js
- node_js:
- - "0.12"
+language: node_js
+node_js:
+ - "5.9"
+env:
+ - CXX=g++-4.8
+dist: trusty
+addons:
+ apt:
+ sources:
+ - ubuntu-toolchain-r-test
+ packages:
+ - g++-4.8
- before_script:
+before_install:
- export DISPLAY=:99.0
- sh -e /etc/init.d/xvfb start
- - npm install --quiet -g grunt-cli karma
- - npm install
+ - npm install --quiet -g karma
- script: grunt
+script: grunt
+sudo: false
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e3094eab15..56e5e8e6ff 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,8 +1,1158 @@
+
+# [2.5.0](https://github.com/angular-ui/bootstrap/compare/2.4.0...v2.5.0) (2017-01-28)
+
+
+### Bug Fixes
+
+* **angular:** add compatibility with Angular 1.6([0d79005](https://github.com/angular-ui/bootstrap/commit/0d79005)), closes [#6427](https://github.com/angular-ui/bootstrap/issues/6427) [#6360](https://github.com/angular-ui/bootstrap/issues/6360)
+* **carousel:** remove transition buffering([86ee770](https://github.com/angular-ui/bootstrap/commit/86ee770)), closes [#6367](https://github.com/angular-ui/bootstrap/issues/6367) [#5967](https://github.com/angular-ui/bootstrap/issues/5967)
+* **dropdown:** do nothing if not open when clicking([761db7b](https://github.com/angular-ui/bootstrap/commit/761db7b)), closes [#6414](https://github.com/angular-ui/bootstrap/issues/6414)
+* **tooltip:** unbind keypress listener on hide([f5b357f](https://github.com/angular-ui/bootstrap/commit/f5b357f)), closes [#6423](https://github.com/angular-ui/bootstrap/issues/6423) [#6405](https://github.com/angular-ui/bootstrap/issues/6405)
+
+
+### Features
+
+* **dropdown:** make dropdown-append-to-body configurable ([#6356](https://github.com/angular-ui/bootstrap/issues/6356))([7d3a750](https://github.com/angular-ui/bootstrap/commit/7d3a750))
+* **pagination:** Added menu and menuitem roles (closes [#6383](https://github.com/angular-ui/bootstrap/issues/6383)) ([#6386](https://github.com/angular-ui/bootstrap/issues/6386))([71dc691](https://github.com/angular-ui/bootstrap/commit/71dc691)), closes [#6383](https://github.com/angular-ui/bootstrap/issues/6383) [(#6386](https://github.com/(/issues/6386)
+
+
+
+
+# [2.4.0](https://github.com/angular-ui/bootstrap/compare/2.3.2...v2.4.0) (2016-12-30)
+
+
+### Features
+
+* **dateparser:** allow overriding of parsers([5a3e44a](https://github.com/angular-ui/bootstrap/commit/5a3e44a)), closes [#6370](https://github.com/angular-ui/bootstrap/issues/6370) [#6373](https://github.com/angular-ui/bootstrap/issues/6373)
+
+
+
+
+## [2.3.2](https://github.com/angular-ui/bootstrap/compare/2.3.1...v2.3.2) (2016-12-27)
+
+
+### Bug Fixes
+
+* **dropdown:** re-add close([955848c](https://github.com/angular-ui/bootstrap/commit/955848c)), closes [#6382](https://github.com/angular-ui/bootstrap/issues/6382) [#6321](https://github.com/angular-ui/bootstrap/issues/6321) [#6357](https://github.com/angular-ui/bootstrap/issues/6357) [#6364](https://github.com/angular-ui/bootstrap/issues/6364)
+
+
+
+
+## [2.3.1](https://github.com/angular-ui/bootstrap/compare/2.3.0...v2.3.1) (2016-12-10)
+
+
+### Bug Fixes
+
+* **dateparser:** add new date format for angular 1.5+ only([f2722b5](https://github.com/angular-ui/bootstrap/commit/f2722b5)), closes [#6349](https://github.com/angular-ui/bootstrap/issues/6349)
+
+* **datepickerPopup:** change to toTimezone only([1962485](https://github.com/angular-ui/bootstrap/commit/1962485)), fixes [#6235](https://github.com/angular-ui/bootstrap/issues/6235)
+
+* **modal:** revert focus behavior on open([8a4f625](https://github.com/angular-ui/bootstrap/commit/8a4f625)), closes [#6295](https://github.com/angular-ui/bootstrap/issues/6295)
+
+
+# [2.3.0](https://github.com/angular-ui/bootstrap/compare/2.2.0...2.3.0) (2016-11-26)
+
+
+### Features
+
+* **dateparser:** add LLLL support([25ff206](https://github.com/angular-ui/bootstrap/commit/25ff206)), closes [#6281](https://github.com/angular-ui/bootstrap/issues/6281)
+
+
+
+
+# [2.2.0](https://github.com/angular-ui/bootstrap/compare/2.1.4...2.2.0) (2016-10-10)
+
+
+### Bug Fixes
+
+* **dropdown:** exit keybind is not open ([14384fc](https://github.com/angular-ui/bootstrap/commit/14384fc)), closes [#6278](https://github.com/angular-ui/bootstrap/issues/6278) [#6208](https://github.com/angular-ui/bootstrap/issues/6208)
+* **modal:** improve ARIA support. ([f9f7e02](https://github.com/angular-ui/bootstrap/commit/f9f7e02)), closes [#6203](https://github.com/angular-ui/bootstrap/issues/6203)
+* **position:** correct scrollbar width calculation ([58f1813](https://github.com/angular-ui/bootstrap/commit/58f1813)), closes [#6273](https://github.com/angular-ui/bootstrap/issues/6273)
+* **tooltip:** cancel timeout when hidden ([7855976](https://github.com/angular-ui/bootstrap/commit/7855976)), closes [#6226](https://github.com/angular-ui/bootstrap/issues/6226) [#6221](https://github.com/angular-ui/bootstrap/issues/6221)
+
+### Features
+
+* **timepicker:** add validation information ([9666c64](https://github.com/angular-ui/bootstrap/commit/9666c64)), closes [#6230](https://github.com/angular-ui/bootstrap/issues/6230) [#6259](https://github.com/angular-ui/bootstrap/issues/6259)
+
+
+
+
+## [2.1.4](https://github.com/angular-ui/bootstrap/compare/2.1.3...2.1.4) (2016-09-24)
+
+
+### Bug Fixes
+
+* **datepicker:** improve accessibility ([3f70d76](https://github.com/angular-ui/bootstrap/commit/3f70d76)), closes [#6247](https://github.com/angular-ui/bootstrap/issues/6247)
+* **dropdown:** prevent premature scope removal ([08ee30a](https://github.com/angular-ui/bootstrap/commit/08ee30a)), closes [#6238](https://github.com/angular-ui/bootstrap/issues/6238) [#6225](https://github.com/angular-ui/bootstrap/issues/6225)
+
+
+
+
+## [2.1.3](https://github.com/angular-ui/bootstrap/compare/2.1.2...2.1.3) (2016-08-25)
+
+
+### Bug Fixes
+
+* **modal:** compile only once with component ([969eb9c](https://github.com/angular-ui/bootstrap/commit/969eb9c)), closes [#6202](https://github.com/angular-ui/bootstrap/issues/6202) [#6201](https://github.com/angular-ui/bootstrap/issues/6201)
+
+
+
+
+## [2.1.2](https://github.com/angular-ui/bootstrap/compare/2.1.1...2.1.2) (2016-08-22)
+
+
+### Bug Fixes
+
+* **collapse:** revert change to transition css ([515bcf2](https://github.com/angular-ui/bootstrap/commit/515bcf2)), closes [#6196](https://github.com/angular-ui/bootstrap/issues/6196) [#6194](https://github.com/angular-ui/bootstrap/issues/6194)
+* **datepicker:** fix accidental global ([ddcacb7](https://github.com/angular-ui/bootstrap/commit/ddcacb7)), closes [#6188](https://github.com/angular-ui/bootstrap/issues/6188)
+* **modal:** close and dismiss bindings on component ([3e8ecff](https://github.com/angular-ui/bootstrap/commit/3e8ecff)), closes [#6192](https://github.com/angular-ui/bootstrap/issues/6192) [#6191](https://github.com/angular-ui/bootstrap/issues/6191)
+
+
+
+
+## [2.1.1](https://github.com/angular-ui/bootstrap/compare/2.1.0...2.1.1) (2016-08-21)
+
+
+### Bug Fixes
+
+* **collapse:** default to css([aef24cd](https://github.com/angular-ui/bootstrap/commit/aef24cd)), closes [#6182](https://github.com/angular-ui/bootstrap/issues/6182) [#6045](https://github.com/angular-ui/bootstrap/issues/6045)
+* **modal:** switch to .append([fb5fabf](https://github.com/angular-ui/bootstrap/commit/fb5fabf)), closes [#6187](https://github.com/angular-ui/bootstrap/issues/6187) [#6186](https://github.com/angular-ui/bootstrap/issues/6186)
+
+
+
+
+# [2.1.0](https://github.com/angular-ui/bootstrap/compare/2.0.2...2.1.0) (2016-08-19)
+
+
+### Bug Fixes
+
+* **collapse:** remove unnecessary inherit ([ca20be4](https://github.com/angular-ui/bootstrap/commit/ca20be4)), closes [#6164](https://github.com/angular-ui/bootstrap/issues/6164) [#6163](https://github.com/angular-ui/bootstrap/issues/6163)
+* **collapse:** set overflow to hidden on transition ([84cc2cf](https://github.com/angular-ui/bootstrap/commit/84cc2cf)), closes [#6180](https://github.com/angular-ui/bootstrap/issues/6180) [#5474](https://github.com/angular-ui/bootstrap/issues/5474)
+* **datepickerPopup:** apply timezone conversion ([f147d22](https://github.com/angular-ui/bootstrap/commit/f147d22)), closes [#6173](https://github.com/angular-ui/bootstrap/issues/6173) [#6147](https://github.com/angular-ui/bootstrap/issues/6147)
+* **modal:** improve ARIA support ([4a5e6a7](https://github.com/angular-ui/bootstrap/commit/4a5e6a7)), closes [#4772](https://github.com/angular-ui/bootstrap/issues/4772)
+* **tooltip:** close tooltip on esc ([f5ff12c](https://github.com/angular-ui/bootstrap/commit/f5ff12c)), closes [#6177](https://github.com/angular-ui/bootstrap/issues/6177) [#6108](https://github.com/angular-ui/bootstrap/issues/6108)
+
+### Features
+
+* **modal:** add component support ([2ade054](https://github.com/angular-ui/bootstrap/commit/2ade054)), closes [#5683](https://github.com/angular-ui/bootstrap/issues/5683) [#6179](https://github.com/angular-ui/bootstrap/issues/6179)
+
+
+
+
+## [2.0.2](https://github.com/angular-ui/bootstrap/compare/2.0.1...2.0.2) (2016-08-15)
+
+
+### Bug Fixes
+
+* **datepickerPopup:** correctly format to timezone ([fbd0845](https://github.com/angular-ui/bootstrap/commit/fbd0845)), closes [#6159](https://github.com/angular-ui/bootstrap/issues/6159) [#6105](https://github.com/angular-ui/bootstrap/issues/6105) [#6146](https://github.com/angular-ui/bootstrap/issues/6146) [#6147](https://github.com/angular-ui/bootstrap/issues/6147)
+* **dropdown:** fix keyboard-nav ([6bad759](https://github.com/angular-ui/bootstrap/commit/6bad759)), closes [#6102](https://github.com/angular-ui/bootstrap/issues/6102) [#6154](https://github.com/angular-ui/bootstrap/issues/6154)
+
+
+
+
+## [2.0.1](https://github.com/angular-ui/bootstrap/compare/2.0.0...2.0.1) (2016-08-02)
+
+
+### Bug Fixes
+
+* **modal:** restore broken stacked modals([c61d16a](https://github.com/angular-ui/bootstrap/commit/c61d16a)), closes [#6103](https://github.com/angular-ui/bootstrap/issues/6103) [#6104](https://github.com/angular-ui/bootstrap/issues/6104)
+
+
+
+
+# [2.0.0](https://github.com/angular-ui/bootstrap/compare/1.3.3...2.0.0) (2016-07-20)
+
+
+### Bug Fixes
+
+* **dateparser:** correctly format with literals([d846e2d](https://github.com/angular-ui/bootstrap/commit/d846e2d)), closes [#6055](https://github.com/angular-ui/bootstrap/issues/6055) [#5620](https://github.com/angular-ui/bootstrap/issues/5620) [#5802](https://github.com/angular-ui/bootstrap/issues/5802)
+* **datepickerPopup:** clear date when button is clicked([b0466d9](https://github.com/angular-ui/bootstrap/commit/b0466d9)), closes [#5945](https://github.com/angular-ui/bootstrap/issues/5945) [#5906](https://github.com/angular-ui/bootstrap/issues/5906)
+* **datepickerPopup:** specify dependency on datepicker([4fef037](https://github.com/angular-ui/bootstrap/commit/4fef037)), closes [#5954](https://github.com/angular-ui/bootstrap/issues/5954)
+* **datepickerPopup:** use value instead of viewValue([7e320e0](https://github.com/angular-ui/bootstrap/commit/7e320e0)), closes [#6007](https://github.com/angular-ui/bootstrap/issues/6007)
+* **dropdown:** align position correctly with scrollbar([2d831bc](https://github.com/angular-ui/bootstrap/commit/2d831bc)), closes [#6008](https://github.com/angular-ui/bootstrap/issues/6008) [#5942](https://github.com/angular-ui/bootstrap/issues/5942)
+* **dropdown:** bind event listener on dropdown menu([6038403](https://github.com/angular-ui/bootstrap/commit/6038403)), closes [#5982](https://github.com/angular-ui/bootstrap/issues/5982)
+* **modal:** check for overflow hidden([433e536](https://github.com/angular-ui/bootstrap/commit/433e536)), closes [#6037](https://github.com/angular-ui/bootstrap/issues/6037) [#6041](https://github.com/angular-ui/bootstrap/issues/6041)
+* **modal:** filter out non-tabbable elements([35ced04](https://github.com/angular-ui/bootstrap/commit/35ced04)), closes [#5963](https://github.com/angular-ui/bootstrap/issues/5963) [#5955](https://github.com/angular-ui/bootstrap/issues/5955)
+* **modal:** remove unused template from modal([1de58a3](https://github.com/angular-ui/bootstrap/commit/1de58a3))
+* **modal:** remove window class after animation([409b7aa](https://github.com/angular-ui/bootstrap/commit/409b7aa)), closes [#6056](https://github.com/angular-ui/bootstrap/issues/6056) [#6051](https://github.com/angular-ui/bootstrap/issues/6051)
+* **tooltip:** missed dependency for cjs([164811a](https://github.com/angular-ui/bootstrap/commit/164811a)), closes [#5999](https://github.com/angular-ui/bootstrap/issues/5999)
+* **tooltip:** reposition for placement top([34a1443](https://github.com/angular-ui/bootstrap/commit/34a1443)), closes [#5959](https://github.com/angular-ui/bootstrap/issues/5959)
+* **typeahead:** change to select class([13c14af](https://github.com/angular-ui/bootstrap/commit/13c14af)), closes [#5922](https://github.com/angular-ui/bootstrap/issues/5922) [#5848](https://github.com/angular-ui/bootstrap/issues/5848)
+* **typeahead:** clear validity in $digest([ed3400b](https://github.com/angular-ui/bootstrap/commit/ed3400b)), closes [#6033](https://github.com/angular-ui/bootstrap/issues/6033) [#6032](https://github.com/angular-ui/bootstrap/issues/6032)
+* **typeahead:** remove duplicate id attribute([6d5b84a](https://github.com/angular-ui/bootstrap/commit/6d5b84a)), closes [#5936](https://github.com/angular-ui/bootstrap/issues/5936) [#5926](https://github.com/angular-ui/bootstrap/issues/5926)
+
+
+### Features
+
+* **accordion:** add appropriate tabindex on disabled([5f4eedd](https://github.com/angular-ui/bootstrap/commit/5f4eedd)), closes [#4067](https://github.com/angular-ui/bootstrap/issues/4067) [#5990](https://github.com/angular-ui/bootstrap/issues/5990)
+* **accordion:** remove replace: true usage([3819bbe](https://github.com/angular-ui/bootstrap/commit/3819bbe)), closes [#5985](https://github.com/angular-ui/bootstrap/issues/5985)
+* **alert:** remove replace: true usage([2458c28](https://github.com/angular-ui/bootstrap/commit/2458c28)), closes [#5986](https://github.com/angular-ui/bootstrap/issues/5986)
+* **carousel:** remove replace: true usage([5046bd4](https://github.com/angular-ui/bootstrap/commit/5046bd4)), closes [#5987](https://github.com/angular-ui/bootstrap/issues/5987)
+* **collapse:** add horizontal support([1ec0997](https://github.com/angular-ui/bootstrap/commit/1ec0997)), closes [#3593](https://github.com/angular-ui/bootstrap/issues/3593) [#6010](https://github.com/angular-ui/bootstrap/issues/6010)
+* **datepicker:** add monthColumns([39d9b98](https://github.com/angular-ui/bootstrap/commit/39d9b98)), closes [#5515](https://github.com/angular-ui/bootstrap/issues/5515) [#5935](https://github.com/angular-ui/bootstrap/issues/5935)
+* **datepicker:** add ngModelOptions support([23b91d9](https://github.com/angular-ui/bootstrap/commit/23b91d9)), closes [#5933](https://github.com/angular-ui/bootstrap/issues/5933) [#5825](https://github.com/angular-ui/bootstrap/issues/5825)
+* **datepicker:** remove replace: true usage([e92fb7f](https://github.com/angular-ui/bootstrap/commit/e92fb7f)), closes [#5988](https://github.com/angular-ui/bootstrap/issues/5988)
+* **datepickerPopup:** remove replace usage([a47bced](https://github.com/angular-ui/bootstrap/commit/a47bced)), closes [#5993](https://github.com/angular-ui/bootstrap/issues/5993)
+* **dropdown:** focus toggle on close from click([cce0097](https://github.com/angular-ui/bootstrap/commit/cce0097)), closes [#5934](https://github.com/angular-ui/bootstrap/issues/5934) [#5782](https://github.com/angular-ui/bootstrap/issues/5782)
+* **dropdown:** use .value() for uibDropdownConfig([7457fb0](https://github.com/angular-ui/bootstrap/commit/7457fb0)), closes [#6004](https://github.com/angular-ui/bootstrap/issues/6004) [#6006](https://github.com/angular-ui/bootstrap/issues/6006)
+* **modal:** append using $animate([1cbd73d](https://github.com/angular-ui/bootstrap/commit/1cbd73d)), closes [#6023](https://github.com/angular-ui/bootstrap/issues/6023) [#6029](https://github.com/angular-ui/bootstrap/issues/6029)
+* **modal:** remove replace usage([96d52ce](https://github.com/angular-ui/bootstrap/commit/96d52ce)), closes [#5989](https://github.com/angular-ui/bootstrap/issues/5989)
+* **pager:** remove replace usage([9b24e1d](https://github.com/angular-ui/bootstrap/commit/9b24e1d)), closes [#5991](https://github.com/angular-ui/bootstrap/issues/5991)
+* **pager:** toggle tabindex when disabled([0d8cec6](https://github.com/angular-ui/bootstrap/commit/0d8cec6)), closes [#5996](https://github.com/angular-ui/bootstrap/issues/5996)
+* **pagination:** disable tabbing when disabled([1a870a3](https://github.com/angular-ui/bootstrap/commit/1a870a3)), closes [#5984](https://github.com/angular-ui/bootstrap/issues/5984)
+* **pagination:** remove replace usage([387c6e7](https://github.com/angular-ui/bootstrap/commit/387c6e7)), closes [#5992](https://github.com/angular-ui/bootstrap/issues/5992)
+* **rating:** remove replace usage([d6fe9c7](https://github.com/angular-ui/bootstrap/commit/d6fe9c7)), closes [#5998](https://github.com/angular-ui/bootstrap/issues/5998)
+* **stackedMap:** improve perf of removeTop([a075824](https://github.com/angular-ui/bootstrap/commit/a075824)), closes [#5925](https://github.com/angular-ui/bootstrap/issues/5925) [#5932](https://github.com/angular-ui/bootstrap/issues/5932)
+* **timepicker:** avoid allowing to tab to controls([4e68778](https://github.com/angular-ui/bootstrap/commit/4e68778)), closes [#5930](https://github.com/angular-ui/bootstrap/issues/5930)
+* **timepicker:** remove replace usage([7518821](https://github.com/angular-ui/bootstrap/commit/7518821)), closes [#5997](https://github.com/angular-ui/bootstrap/issues/5997)
+* **tooltip:** add expression support([4b91222](https://github.com/angular-ui/bootstrap/commit/4b91222)), closes [#5221](https://github.com/angular-ui/bootstrap/issues/5221) [#5938](https://github.com/angular-ui/bootstrap/issues/5938)
+* **tooltip:** remove replace usage([1616e97](https://github.com/angular-ui/bootstrap/commit/1616e97)), closes [#5994](https://github.com/angular-ui/bootstrap/issues/5994)
+
+
+### Reverts
+
+* **dropdown:** change back to .constant()([4e0e34f](https://github.com/angular-ui/bootstrap/commit/4e0e34f))
+
+
+### BREAKING CHANGES
+
+* tooltip: The template structure changed slightly due to the removal of `replace: true` - see documentation examples in action to see differences in practice.
+* typeahead: This changes the selector used so that it doesn't select for the `li` tag specifically, but the elements with the class `uib-typeahead-match` - if using a custom template, this class needs to be added to the `li` element in the typeahead popup template.
+* rating: Due to the removal of `replace: true`, this results in a slight change to the HTML structure - see the documentation examples to see it in action.
+* timepicker: This removes `replace: true`, which changes the HTML structure slightly - see the documentation examples to see it in action.
+* datepickerPopup: Due to the nature of `replace: true`, this has a slight structural HTML change in the popup as a result - see documentation examples for the change in action.
+* pagination: Due to the removal of `replace: true`, this affects the HTML structure slightly - see documentation examples to see new usage.
+* pager: This removes `replace: true` usage from the pager, which causes a slight usage change - see documentation examples for new usage.
+* modal: This removes `replace: true` usage, causing some structural changes to the HTML - the major part here is that there is no more backdrop template, and the top level elements for the window/backdrop elements lose a little flexibility. See documentation examples for new structure.
+* modal: This introduces a minor behavior change in when the class is removed
+* carousel: Due to the removal of `replace: true`, this causes a slight HTML structure change to the carousel and the slide elements - see documentation demos to see how it changes. This also caused removal of the ngTouch built in support - if one is using ng-touch, one needs to add the `ng-swipe-left` and `ng-swipe-right` directives to the carousel element with relevant logic.
+* alert: This removes the `replace: true` usage, so this has an effect on how one uses the directive in the template - see documentation for reference
+* accordion: This removes usage of `replace: true` in the accordion group, which results in a template change where the template no longer needs to contain the panel itself, but its contents. The accordion group will add the `panel` class by default, so the user just needs to add the appropriate classes to the accordion group element. This allows the user to use ng-class as well to fully control the panel related classes, so `panel-class` now is unnecessary
+* tooltip: This removes support for plain strings and interpolations in tooltip-trigger and popover-trigger - please change these appropriately. See test changes in this commit for reference
+* typeahead: This change removes the `id` attribute on the first ``
+element placed into the DOM when the `typeahead-show-hint` attribute is used
+and there is an `id` attribute present on the original `uib-typeahead` element.
+This could affect selectors if they are being used.
+* dropdown: This changes the focus behavior of the dropdown slightly, and potentially may break code built around current usage
+* datepicker: This modifies the current behavior around the datepicker & popup's ngModelOptions, which may affect custom components that are built around both
+* datepicker: As a result of removal of `replace: true`, there is the potential that this may break some CSS layout due to the slightly different HTML. Refer to the documentation examples to see the new structure.
+
+
+
+
+## [1.3.3](https://github.com/angular-ui/bootstrap/compare/1.3.2...1.3.3) (2016-05-23)
+
+
+### Bug Fixes
+
+* **datepicker:** add intermediary valid date check([a417ec2](https://github.com/angular-ui/bootstrap/commit/a417ec2)), closes [#5872](https://github.com/angular-ui/bootstrap/issues/5872) [#5865](https://github.com/angular-ui/bootstrap/issues/5865)
+* **datepickerPopup:** convert numbers to date before parsing([5188463](https://github.com/angular-ui/bootstrap/commit/5188463)), closes [#5873](https://github.com/angular-ui/bootstrap/issues/5873) [#5871](https://github.com/angular-ui/bootstrap/issues/5871)
+* **dropdown:** align position with vertical scrollbar([2b48259](https://github.com/angular-ui/bootstrap/commit/2b48259)), closes [#5830](https://github.com/angular-ui/bootstrap/issues/5830) [#4317](https://github.com/angular-ui/bootstrap/issues/4317)
+
+
+### Features
+
+* **accordion:** add alternative attribute support([4c40d9d](https://github.com/angular-ui/bootstrap/commit/4c40d9d)), closes [#5834](https://github.com/angular-ui/bootstrap/issues/5834) [#5839](https://github.com/angular-ui/bootstrap/issues/5839)
+* **modal:** add resolve values to template([dd09148](https://github.com/angular-ui/bootstrap/commit/dd09148)), closes [#5808](https://github.com/angular-ui/bootstrap/issues/5808) [#5857](https://github.com/angular-ui/bootstrap/issues/5857)
+* **tab:** allow strings for index([26d3995](https://github.com/angular-ui/bootstrap/commit/26d3995)), closes [#5713](https://github.com/angular-ui/bootstrap/issues/5713) [#5827](https://github.com/angular-ui/bootstrap/issues/5827)
+* **tabs:** pass the selected index to onDeselect([241fea8](https://github.com/angular-ui/bootstrap/commit/241fea8)), closes [#5820](https://github.com/angular-ui/bootstrap/issues/5820) [#5821](https://github.com/angular-ui/bootstrap/issues/5821)
+* **typeahead:** Add support for `should-select`([2ffb86f](https://github.com/angular-ui/bootstrap/commit/2ffb86f)), closes [#5671](https://github.com/angular-ui/bootstrap/issues/5671) [#5794](https://github.com/angular-ui/bootstrap/issues/5794)
+
+
+### BREAKING CHANGES
+
+* modal: Since this adds support for $resolve being exposed on $scope, it could potentially overwrite any pre-existing usage of it - this is an unlikely scenario, but marked as a breaking change in case this key is being used
+
+
+
+
+## [1.3.2](https://github.com/angular-ui/bootstrap/compare/1.3.1...1.3.2) (2016-04-14)
+
+
+### Bug Fixes
+
+* **datepicker:** ensure datepicker is focused ([33cbd0f](https://github.com/angular-ui/bootstrap/commit/33cbd0f)), closes [#5761](https://github.com/angular-ui/bootstrap/issues/5761) [#5754](https://github.com/angular-ui/bootstrap/issues/5754)
+* **datepickerPopup:** do not show incorrect warning ([98eb8f1](https://github.com/angular-ui/bootstrap/commit/98eb8f1)), closes [#5743](https://github.com/angular-ui/bootstrap/issues/5743) [#5747](https://github.com/angular-ui/bootstrap/issues/5747)
+* **dropdown:** stop esc keydown event ([68200bb](https://github.com/angular-ui/bootstrap/commit/68200bb)), closes [#5778](https://github.com/angular-ui/bootstrap/issues/5778) [#5787](https://github.com/angular-ui/bootstrap/issues/5787)
+* **modal:** missing require in modal ([a887d2b](https://github.com/angular-ui/bootstrap/commit/a887d2b)), closes [#5786](https://github.com/angular-ui/bootstrap/issues/5786)
+* **modal:** scroll padding only added once ([aef08d5](https://github.com/angular-ui/bootstrap/commit/aef08d5)), closes [#5790](https://github.com/angular-ui/bootstrap/issues/5790) [#5789](https://github.com/angular-ui/bootstrap/issues/5789)
+* **site:** allow FastClick to be blocked ([37f9cd2](https://github.com/angular-ui/bootstrap/commit/37f9cd2)), closes [#5756](https://github.com/angular-ui/bootstrap/issues/5756)
+* **tooltip:** arrow position ([d96d53e](https://github.com/angular-ui/bootstrap/commit/d96d53e)), closes [#5785](https://github.com/angular-ui/bootstrap/issues/5785) [#5779](https://github.com/angular-ui/bootstrap/issues/5779)
+* **typeahead:** use $setViewValue on blur ([bba3f27](https://github.com/angular-ui/bootstrap/commit/bba3f27)), closes [#5769](https://github.com/angular-ui/bootstrap/issues/5769) [#5694](https://github.com/angular-ui/bootstrap/issues/5694)
+
+### Features
+
+* **modal:** add no css import script ([a27a4e2](https://github.com/angular-ui/bootstrap/commit/a27a4e2)), closes [#5788](https://github.com/angular-ui/bootstrap/issues/5788)
+
+
+### BREAKING CHANGES
+
+* dropdown: Stops propagation of keydown event when escape key is pressed. Removes keydown event from the document and moves it to the dropdown element.
+
+
+
+
+## [1.3.1](https://github.com/angular-ui/bootstrap/compare/1.3.0...1.3.1) (2016-04-05)
+
+
+### Bug Fixes
+
+* **datepickerPopup:** register popup module ([86ee9c3](https://github.com/angular-ui/bootstrap/commit/86ee9c3)), closes [#5745](https://github.com/angular-ui/bootstrap/issues/5745)
+* **modal:** ensure correct index is set ([a08ad70](https://github.com/angular-ui/bootstrap/commit/a08ad70)), closes [#5733](https://github.com/angular-ui/bootstrap/issues/5733) [#5670](https://github.com/angular-ui/bootstrap/issues/5670)
+
+
+
+
+# [1.3.0](https://github.com/angular-ui/bootstrap/compare/1.2.5...1.3.0) (2016-04-05)
+
+
+### Bug Fixes
+
+* **carousel:** fix animation from programmatic trigger ([44fb19b](https://github.com/angular-ui/bootstrap/commit/44fb19b)), closes [#5674](https://github.com/angular-ui/bootstrap/issues/5674) [#5601](https://github.com/angular-ui/bootstrap/issues/5601)
+* modify CSS loaded for modules with dependencies ([8074899](https://github.com/angular-ui/bootstrap/commit/8074899)), closes [#5698](https://github.com/angular-ui/bootstrap/issues/5698)
+* **carousel:** use proper sort comparison ([434c1c5](https://github.com/angular-ui/bootstrap/commit/434c1c5)), closes [#5638](https://github.com/angular-ui/bootstrap/issues/5638) [#5702](https://github.com/angular-ui/bootstrap/issues/5702)
+* **modal:** body content shift ([c83d0a8](https://github.com/angular-ui/bootstrap/commit/c83d0a8)), closes [#2631](https://github.com/angular-ui/bootstrap/issues/2631) [#5711](https://github.com/angular-ui/bootstrap/issues/5711)
+* **popover:** rename title attribute ([cca1460](https://github.com/angular-ui/bootstrap/commit/cca1460)), closes [#5719](https://github.com/angular-ui/bootstrap/issues/5719) [#5721](https://github.com/angular-ui/bootstrap/issues/5721)
+* **tab:** add support for tab deselect prevention ([e0fcc00](https://github.com/angular-ui/bootstrap/commit/e0fcc00)), closes [#5720](https://github.com/angular-ui/bootstrap/issues/5720) [#5723](https://github.com/angular-ui/bootstrap/issues/5723)
+* **tab:** correctly identify index on removal ([03e6047](https://github.com/angular-ui/bootstrap/commit/03e6047)), closes [#5689](https://github.com/angular-ui/bootstrap/issues/5689)
+* **tab:** fix event handler call ([d767afb](https://github.com/angular-ui/bootstrap/commit/d767afb)), closes [#5720](https://github.com/angular-ui/bootstrap/issues/5720) [#5738](https://github.com/angular-ui/bootstrap/issues/5738)
+
+### Features
+
+* add support for loading without CSS ([de504fb](https://github.com/angular-ui/bootstrap/commit/de504fb)), closes [#5696](https://github.com/angular-ui/bootstrap/issues/5696)
+* **carousel:** disable prev/next arrows if at bounds ([a3964d4](https://github.com/angular-ui/bootstrap/commit/a3964d4)), closes [#5704](https://github.com/angular-ui/bootstrap/issues/5704) [#5708](https://github.com/angular-ui/bootstrap/issues/5708)
+* **dateparser:** use 68 as the yy format pivot year ([701e0cb](https://github.com/angular-ui/bootstrap/commit/701e0cb)), closes [#5735](https://github.com/angular-ui/bootstrap/issues/5735)
+* **datepicker:** deprecate literal usage ([fc02fd1](https://github.com/angular-ui/bootstrap/commit/fc02fd1)), closes [#5658](https://github.com/angular-ui/bootstrap/issues/5658) [#5732](https://github.com/angular-ui/bootstrap/issues/5732)
+* **datepicker:** remove deprecated code ([ad91c82](https://github.com/angular-ui/bootstrap/commit/ad91c82)), closes [#5660](https://github.com/angular-ui/bootstrap/issues/5660)
+* **datepicker:** watch for changes when falsy ([45165ba](https://github.com/angular-ui/bootstrap/commit/45165ba)), closes [#5672](https://github.com/angular-ui/bootstrap/issues/5672) [#5677](https://github.com/angular-ui/bootstrap/issues/5677)
+* **datepickerPopup:** split off popup from datepicker ([fbc873c](https://github.com/angular-ui/bootstrap/commit/fbc873c)), closes [#5676](https://github.com/angular-ui/bootstrap/issues/5676)
+* **dropdown:** remove $locationChangeSuccess listener ([c534cb4](https://github.com/angular-ui/bootstrap/commit/c534cb4)), closes [#5648](https://github.com/angular-ui/bootstrap/issues/5648) [#5680](https://github.com/angular-ui/bootstrap/issues/5680)
+* **timepicker:** remove automatic padding when empty ([449c0d1](https://github.com/angular-ui/bootstrap/commit/449c0d1)), closes [#5293](https://github.com/angular-ui/bootstrap/issues/5293) [#5299](https://github.com/angular-ui/bootstrap/issues/5299)
+
+
+### BREAKING CHANGES
+
+* carousel: This adds disabled CSS - there is a possibility this may break existing UI slightly for those adding custom CSS/making use of custom templates. Modify the template appropriately if necessary
+* timepicker: This removes automatic padding done by the timepicker
+when the input is empty - if that behavior is desired, write a custom
+directive implementing it
+* dropdown: The dropdown no longer will remain open on $locationChangeSuccess with autoclose set to disabled. Implement this logic inside app along with usage of isOpen two-way binding if this functionality is desired.
+* datepickerPopup: The datepicker popup is no longer a part of the
+datepicker module, but now a part of the datepickerPopup module. Please
+change code accordingly if including specific modules
+* datepicker: This removes inline datepicker attribute support and
+attribute pass-throughs in the popup
+
+
+
+
+## [1.2.5](https://github.com/angular-ui/bootstrap/compare/1.2.4...1.2.5) (2016-03-20)
+
+
+### Bug Fixes
+
+* **datepicker:** dereference date initialization ([1b888d4](https://github.com/angular-ui/bootstrap/commit/1b888d4)), closes [#5643](https://github.com/angular-ui/bootstrap/issues/5643) [#5441](https://github.com/angular-ui/bootstrap/issues/5441)
+* **datepicker:** fix today button disabled condition ([316e96c](https://github.com/angular-ui/bootstrap/commit/316e96c)), closes [#5637](https://github.com/angular-ui/bootstrap/issues/5637) [#5622](https://github.com/angular-ui/bootstrap/issues/5622)
+* **modal:** dynamically fetch elements ([e15a22a](https://github.com/angular-ui/bootstrap/commit/e15a22a)), closes [#5630](https://github.com/angular-ui/bootstrap/issues/5630) [#5050](https://github.com/angular-ui/bootstrap/issues/5050) [#5421](https://github.com/angular-ui/bootstrap/issues/5421)
+* **popover:** add popover-html css ([54ac06a](https://github.com/angular-ui/bootstrap/commit/54ac06a)), closes [#5603](https://github.com/angular-ui/bootstrap/issues/5603) [#5602](https://github.com/angular-ui/bootstrap/issues/5602)
+* **tooltip:** css to support arrow positioning ([66c416c](https://github.com/angular-ui/bootstrap/commit/66c416c)), closes [#5610](https://github.com/angular-ui/bootstrap/issues/5610) [#5611](https://github.com/angular-ui/bootstrap/issues/5611)
+* **typeahead:** reset errors when clearing ([bc7c55a](https://github.com/angular-ui/bootstrap/commit/bc7c55a)), closes [#5641](https://github.com/angular-ui/bootstrap/issues/5641)
+
+### Features
+
+* **accordion:** add bound panel-class support ([ffb5529](https://github.com/angular-ui/bootstrap/commit/ffb5529)), closes [#5368](https://github.com/angular-ui/bootstrap/issues/5368) [#5596](https://github.com/angular-ui/bootstrap/issues/5596)
+* **modal:** exclude hidden elements from query ([5ef56e2](https://github.com/angular-ui/bootstrap/commit/5ef56e2)), closes [#5512](https://github.com/angular-ui/bootstrap/issues/5512) [#5644](https://github.com/angular-ui/bootstrap/issues/5644)
+* **rating:** ability to disable rating reset ([8747b58](https://github.com/angular-ui/bootstrap/commit/8747b58)), closes [#5532](https://github.com/angular-ui/bootstrap/issues/5532) [#5631](https://github.com/angular-ui/bootstrap/issues/5631)
+* **tab:** add select expressions ([bb36e40](https://github.com/angular-ui/bootstrap/commit/bb36e40)), closes [#5438](https://github.com/angular-ui/bootstrap/issues/5438)
+* **timepicker:** add pad-hours support ([c7be087](https://github.com/angular-ui/bootstrap/commit/c7be087)), closes [#4288](https://github.com/angular-ui/bootstrap/issues/4288) [#5633](https://github.com/angular-ui/bootstrap/issues/5633)
+
+
+
+
+## [1.2.4](https://github.com/angular-ui/bootstrap/compare/1.2.3...1.2.4) (2016-03-06)
+
+
+
+
+
+## [1.2.3](https://github.com/angular-ui/bootstrap/compare/1.2.2...1.2.3) (2016-03-06)
+
+
+### Bug Fixes
+
+* **dropdown:** correctly update isOpen ([aac0d2b](https://github.com/angular-ui/bootstrap/commit/aac0d2b)), closes [#5589](https://github.com/angular-ui/bootstrap/issues/5589) [#3261](https://github.com/angular-ui/bootstrap/issues/3261)
+* **modal:** switch to $animate ([dd62c73](https://github.com/angular-ui/bootstrap/commit/dd62c73)), closes [#5585](https://github.com/angular-ui/bootstrap/issues/5585) [#5298](https://github.com/angular-ui/bootstrap/issues/5298)
+* **position:** auto position append to body ([a516178](https://github.com/angular-ui/bootstrap/commit/a516178)), closes [#5582](https://github.com/angular-ui/bootstrap/issues/5582) [#5576](https://github.com/angular-ui/bootstrap/issues/5576)
+* **tabs:** support heading with : ([a945dd2](https://github.com/angular-ui/bootstrap/commit/a945dd2)), closes [#5590](https://github.com/angular-ui/bootstrap/issues/5590) [#5568](https://github.com/angular-ui/bootstrap/issues/5568)
+* **typeahead:** change to original scope ([7949494](https://github.com/angular-ui/bootstrap/commit/7949494)), closes [#5588](https://github.com/angular-ui/bootstrap/issues/5588) [#5467](https://github.com/angular-ui/bootstrap/issues/5467)
+* **typeahead:** update isOpen on escape ([313ba83](https://github.com/angular-ui/bootstrap/commit/313ba83)), closes [#5579](https://github.com/angular-ui/bootstrap/issues/5579) [#5587](https://github.com/angular-ui/bootstrap/issues/5587)
+
+### Features
+
+* **datepicker:** add helpers for styling ([c15dcbd](https://github.com/angular-ui/bootstrap/commit/c15dcbd)), closes [#5580](https://github.com/angular-ui/bootstrap/issues/5580)
+
+
+
+
+## [1.2.2](https://github.com/angular-ui/bootstrap/compare/1.2.1...1.2.2) (2016-03-03)
+
+
+### Bug Fixes
+
+* **Gruntfile:** add per module flag ([a816251](https://github.com/angular-ui/bootstrap/commit/a816251)), closes [#5567](https://github.com/angular-ui/bootstrap/issues/5567)
+* **modal:** fire ng-leave on close ([299f751](https://github.com/angular-ui/bootstrap/commit/299f751)), closes [#5556](https://github.com/angular-ui/bootstrap/issues/5556) [#5399](https://github.com/angular-ui/bootstrap/issues/5399)
+* **modal:** fix bindToController props ([5e43870](https://github.com/angular-ui/bootstrap/commit/5e43870)), closes [#5569](https://github.com/angular-ui/bootstrap/issues/5569)
+* **position:** add CSS for webpack support ([d68086f](https://github.com/angular-ui/bootstrap/commit/d68086f)), closes [#5561](https://github.com/angular-ui/bootstrap/issues/5561)
+* **rating:** fix usage of aria-valuetext ([4369800](https://github.com/angular-ui/bootstrap/commit/4369800)), closes [#5573](https://github.com/angular-ui/bootstrap/issues/5573) [#5571](https://github.com/angular-ui/bootstrap/issues/5571)
+* **tabs:** update doc and fix tab demo ([9d74d6c](https://github.com/angular-ui/bootstrap/commit/9d74d6c)), closes [#5575](https://github.com/angular-ui/bootstrap/issues/5575) [#5551](https://github.com/angular-ui/bootstrap/issues/5551)
+* **tests:** add style tag once ([cc2d1b9](https://github.com/angular-ui/bootstrap/commit/cc2d1b9)), closes [#5557](https://github.com/angular-ui/bootstrap/issues/5557) [#5548](https://github.com/angular-ui/bootstrap/issues/5548)
+* **tooltip:** import CSS ([c8922a2](https://github.com/angular-ui/bootstrap/commit/c8922a2)), closes [#5552](https://github.com/angular-ui/bootstrap/issues/5552)
+
+
+
+
+## [1.2.1](https://github.com/angular-ui/bootstrap/compare/1.2.0...1.2.1) (2016-02-27)
+
+
+### Bug Fixes
+
+* **popover:** add missing selector ([70c1c90](https://github.com/angular-ui/bootstrap/commit/70c1c90)), closes [#5549](https://github.com/angular-ui/bootstrap/issues/5549)
+
+
+
+
+# [1.2.0](https://github.com/angular-ui/bootstrap/compare/1.1.2...1.2.0) (2016-02-26)
+
+
+### Bug Fixes
+
+* **buttons:** fix uncheckable attribute ([b245242](https://github.com/angular-ui/bootstrap/commit/b245242)), closes [#5451](https://github.com/angular-ui/bootstrap/issues/5451) [#5412](https://github.com/angular-ui/bootstrap/issues/5412)
+* **dateparser:** append end of format ([5c565df](https://github.com/angular-ui/bootstrap/commit/5c565df)), closes [#5385](https://github.com/angular-ui/bootstrap/issues/5385) [#5387](https://github.com/angular-ui/bootstrap/issues/5387)
+* **datepicker:** adjust datepicker header buttons ([f125537](https://github.com/angular-ui/bootstrap/commit/f125537)), closes [#5393](https://github.com/angular-ui/bootstrap/issues/5393) [#5392](https://github.com/angular-ui/bootstrap/issues/5392)
+* **datepicker:** assign initDate correctly to the controller ([8769749](https://github.com/angular-ui/bootstrap/commit/8769749)), closes [#5541](https://github.com/angular-ui/bootstrap/issues/5541)
+* **datepicker:** check if initDate is valid ([ab59413](https://github.com/angular-ui/bootstrap/commit/ab59413)), closes [#5190](https://github.com/angular-ui/bootstrap/issues/5190) [#5266](https://github.com/angular-ui/bootstrap/issues/5266)
+* **datepicker:** default to current date ([4b2ee0f](https://github.com/angular-ui/bootstrap/commit/4b2ee0f)), closes [#5395](https://github.com/angular-ui/bootstrap/issues/5395) [#5190](https://github.com/angular-ui/bootstrap/issues/5190)
+* **datepicker:** fallback to current date ([e4fc201](https://github.com/angular-ui/bootstrap/commit/e4fc201)), closes [#5418](https://github.com/angular-ui/bootstrap/issues/5418) [#5417](https://github.com/angular-ui/bootstrap/issues/5417)
+* **datepicker:** fix popup updateOn: default support ([47e412e](https://github.com/angular-ui/bootstrap/commit/47e412e)), closes [#5529](https://github.com/angular-ui/bootstrap/issues/5529)
+* **datepicker:** stop event bubbling from buttons ([e283cea](https://github.com/angular-ui/bootstrap/commit/e283cea)), closes [#5400](https://github.com/angular-ui/bootstrap/issues/5400) [#5347](https://github.com/angular-ui/bootstrap/issues/5347)
+* **modal:** fix modal rendered promise ([42fb486](https://github.com/angular-ui/bootstrap/commit/42fb486)), closes [#5401](https://github.com/angular-ui/bootstrap/issues/5401) [#5331](https://github.com/angular-ui/bootstrap/issues/5331)
+* **modal:** fix race condition with openedClass ([979fe0b](https://github.com/angular-ui/bootstrap/commit/979fe0b)), closes [#5483](https://github.com/angular-ui/bootstrap/issues/5483)
+* **position:** getRawNode to handle select/ul elem ([8b3e86f](https://github.com/angular-ui/bootstrap/commit/8b3e86f)), closes [#5491](https://github.com/angular-ui/bootstrap/issues/5491) [#5354](https://github.com/angular-ui/bootstrap/issues/5354)
+* **position:** move inline styles to a class ([4bb178a](https://github.com/angular-ui/bootstrap/commit/4bb178a)), closes [#5535](https://github.com/angular-ui/bootstrap/issues/5535) [#5470](https://github.com/angular-ui/bootstrap/issues/5470)
+* **progressbar:** fix default max value ([258b341](https://github.com/angular-ui/bootstrap/commit/258b341)), closes [#5374](https://github.com/angular-ui/bootstrap/issues/5374) [#5373](https://github.com/angular-ui/bootstrap/issues/5373)
+* **rating:** change to read-only ([d5a757d](https://github.com/angular-ui/bootstrap/commit/d5a757d)), closes [#5371](https://github.com/angular-ui/bootstrap/issues/5371) [#5435](https://github.com/angular-ui/bootstrap/issues/5435)
+* **tab:** make active optional ([d1553a4](https://github.com/angular-ui/bootstrap/commit/d1553a4)), closes [#5489](https://github.com/angular-ui/bootstrap/issues/5489)
+* **tabs:** adding bootstrap 4 specific class ([3814fe3](https://github.com/angular-ui/bootstrap/commit/3814fe3)), closes [#5488](https://github.com/angular-ui/bootstrap/issues/5488)
+* **typeahead:** fix shift tab ([64e3127](https://github.com/angular-ui/bootstrap/commit/64e3127)), closes [#5494](https://github.com/angular-ui/bootstrap/issues/5494) [#5493](https://github.com/angular-ui/bootstrap/issues/5493)
+* **typeahead:** Fix tab index for hint input ([4178500](https://github.com/angular-ui/bootstrap/commit/4178500)), closes [#5478](https://github.com/angular-ui/bootstrap/issues/5478) [#5492](https://github.com/angular-ui/bootstrap/issues/5492)
+
+### Features
+
+* **accordion:** use attribute for heading insertion ([ca6b177](https://github.com/angular-ui/bootstrap/commit/ca6b177)), closes [#5324](https://github.com/angular-ui/bootstrap/issues/5324) [#5396](https://github.com/angular-ui/bootstrap/issues/5396)
+* **datepicker:** add deprecation notices ([10eac7c](https://github.com/angular-ui/bootstrap/commit/10eac7c)), closes [#5415](https://github.com/angular-ui/bootstrap/issues/5415)
+* **datepicker:** implement auto position ([f9903ba](https://github.com/angular-ui/bootstrap/commit/f9903ba)), closes [#5444](https://github.com/angular-ui/bootstrap/issues/5444)
+* **datepicker:** move attribute support to options ([d880aea](https://github.com/angular-ui/bootstrap/commit/d880aea)), closes [#5528](https://github.com/angular-ui/bootstrap/issues/5528)
+* **datepicker:** move datepicker mode to options ([74a1d04](https://github.com/angular-ui/bootstrap/commit/74a1d04)), closes [#5526](https://github.com/angular-ui/bootstrap/issues/5526)
+* **datepicker:** pass options from popup to inline ([3e10184](https://github.com/angular-ui/bootstrap/commit/3e10184)), closes [#5355](https://github.com/angular-ui/bootstrap/issues/5355)
+* **datepicker:** preserve popup attributes ([bbc4912](https://github.com/angular-ui/bootstrap/commit/bbc4912)), closes [#5537](https://github.com/angular-ui/bootstrap/issues/5537)
+* **modal:** Call $onInit on controller if defined ([8349758](https://github.com/angular-ui/bootstrap/commit/8349758)), closes [#5472](https://github.com/angular-ui/bootstrap/issues/5472) [#5509](https://github.com/angular-ui/bootstrap/issues/5509)
+* **pagination:** add custom label support ([785c373](https://github.com/angular-ui/bootstrap/commit/785c373)), closes [#2532](https://github.com/angular-ui/bootstrap/issues/2532) [#5547](https://github.com/angular-ui/bootstrap/issues/5547)
+* **position:** add isScrollable ([3c0a7cd](https://github.com/angular-ui/bootstrap/commit/3c0a7cd)), closes [#5508](https://github.com/angular-ui/bootstrap/issues/5508)
+* **tab:** add template-url support ([54c51c4](https://github.com/angular-ui/bootstrap/commit/54c51c4)), closes [#5405](https://github.com/angular-ui/bootstrap/issues/5405) [#5443](https://github.com/angular-ui/bootstrap/issues/5443)
+* **tabs:** support optional classes on tab ([8364e76](https://github.com/angular-ui/bootstrap/commit/8364e76)), closes [#5430](https://github.com/angular-ui/bootstrap/issues/5430)
+* **tooltip:** arrow position ([fa17c48](https://github.com/angular-ui/bootstrap/commit/fa17c48)), closes [#5464](https://github.com/angular-ui/bootstrap/issues/5464) [#5502](https://github.com/angular-ui/bootstrap/issues/5502)
+* **typeahead:** add dynamic min length support ([f81d440](https://github.com/angular-ui/bootstrap/commit/f81d440)), closes [#5363](https://github.com/angular-ui/bootstrap/issues/5363)
+
+
+### BREAKING CHANGES
+
+* rating: Attribute supported has been changed to `read-only`
+from `readonly`
+* accordion: This changes to use the `uibAccordionHeader` attribute instead of a `` element for inserting the custom header HTML. If you use a custom template for the accordion group, please add this attribute to the appropriate location.
+* datepicker: This requires $event to be passed in the second
+argument of the select function and in the close argument for the popup
+template
+* datepicker: This adds extra CSS for the datepicker for the left and right header buttons - one can override this appropriately with any selector of class priority higher than 1
+* datepicker: This breaks any snake-cased key usage, i.e.
+`show-weeks`. Convert all keys used to camelCase.
+* tab: Make the `active` attribute optional and move to `tabset` element.
+* carousel: the `active` attribute is now required on the `uib-carousel` element and a unique `index` attribute is required on each `uib-slide` element.
+
+
+
+
+## [1.1.2](https://github.com/angular-ui/bootstrap/compare/1.1.1...1.1.2) (2016-02-01)
+
+
+### Bug Fixes
+
+* **datepicker:** fix validation for M! & d! ([c3b1431](https://github.com/angular-ui/bootstrap/commit/c3b1431)), closes [#5376](https://github.com/angular-ui/bootstrap/issues/5376) [#5225](https://github.com/angular-ui/bootstrap/issues/5225)
+* **modal:** ensure shift+tab is trapped in modal ([d2621e3](https://github.com/angular-ui/bootstrap/commit/d2621e3)), closes [#5294](https://github.com/angular-ui/bootstrap/issues/5294) [#5229](https://github.com/angular-ui/bootstrap/issues/5229)
+* **tooltip:** prevent closing on $locationChangeSuccess ([48c9cd8](https://github.com/angular-ui/bootstrap/commit/48c9cd8)), closes [#5360](https://github.com/angular-ui/bootstrap/issues/5360) [#5337](https://github.com/angular-ui/bootstrap/issues/5337)
+* **typeahead:** add guard for no $viewValue ([dbe9e81](https://github.com/angular-ui/bootstrap/commit/dbe9e81)), closes [#5358](https://github.com/angular-ui/bootstrap/issues/5358) [#5357](https://github.com/angular-ui/bootstrap/issues/5357)
+
+### Features
+
+* **accordion:** add aria tags ([61cdef1](https://github.com/angular-ui/bootstrap/commit/61cdef1)), closes [#5338](https://github.com/angular-ui/bootstrap/issues/5338)
+* **datepicker:** add datepickerOptions support ([e58c42c](https://github.com/angular-ui/bootstrap/commit/e58c42c)), closes [#5340](https://github.com/angular-ui/bootstrap/issues/5340)
+
+
+
+
+## [1.1.1](https://github.com/angular-ui/bootstrap/compare/1.1.0...1.1.1) (2016-01-25)
+
+
+### Bug Fixes
+
+* **datepicker:** allow using min/max mode/date with datepickerOptions ([97af6a9](https://github.com/angular-ui/bootstrap/commit/97af6a9)), closes [#5334](https://github.com/angular-ui/bootstrap/issues/5334) [#5315](https://github.com/angular-ui/bootstrap/issues/5315)
+* **modal:** fix modal closed resolution ([d5a48ea](https://github.com/angular-ui/bootstrap/commit/d5a48ea)), closes [#5322](https://github.com/angular-ui/bootstrap/issues/5322) [#5326](https://github.com/angular-ui/bootstrap/issues/5326)
+
+
+
+
+# [1.1.0](https://github.com/angular-ui/bootstrap/compare/v1.0.3...v1.1.0) (2016-01-18)
+
+
+## Bug Fixes
+
+* **collapse:** avoid initial animation when expanded ([57219aa](https://github.com/angular-ui/bootstrap/commit/57219aa)), closes [#5199](https://github.com/angular-ui/bootstrap/issues/5199) [#4414](https://github.com/angular-ui/bootstrap/issues/4414) [#5192](https://github.com/angular-ui/bootstrap/issues/5192)
+* **datepicker:** allow expressions for minMode/maxMode ([7e93ec9](https://github.com/angular-ui/bootstrap/commit/7e93ec9)), closes [#5264](https://github.com/angular-ui/bootstrap/issues/5264) [#5268](https://github.com/angular-ui/bootstrap/issues/5268) [#5240](https://github.com/angular-ui/bootstrap/issues/5240)
+* **datepicker:** fix popup CSS ([b3e6d17](https://github.com/angular-ui/bootstrap/commit/b3e6d17)), closes [#5258](https://github.com/angular-ui/bootstrap/issues/5258)
+* **datepicker:** fix popup element garbage collection ([2cb3bc2](https://github.com/angular-ui/bootstrap/commit/2cb3bc2)), closes [#5274](https://github.com/angular-ui/bootstrap/issues/5274) [#5058](https://github.com/angular-ui/bootstrap/issues/5058)
+* **datepicker:** pass through null ([78ba137](https://github.com/angular-ui/bootstrap/commit/78ba137)), closes [#5275](https://github.com/angular-ui/bootstrap/issues/5275) [#5238](https://github.com/angular-ui/bootstrap/issues/5238)
+* **modal:** change to compile before appending ([1dbad8a](https://github.com/angular-ui/bootstrap/commit/1dbad8a)), closes [#5224](https://github.com/angular-ui/bootstrap/issues/5224) [#5183](https://github.com/angular-ui/bootstrap/issues/5183)
+* **paging:** garbage collect parent $watchers ([32b88f1](https://github.com/angular-ui/bootstrap/commit/32b88f1)), closes [#5276](https://github.com/angular-ui/bootstrap/issues/5276)
+* **position:** positionArrow descendant selector ([9f4c3a5](https://github.com/angular-ui/bootstrap/commit/9f4c3a5), [e8b6a83](https://github.com/angular-ui/bootstrap/commit/e8b6a83)), closes [#5246](https://github.com/angular-ui/bootstrap/issues/5246) [#5230](https://github.com/angular-ui/bootstrap/issues/5230)
+* **tab:** revert template change ([907c851](https://github.com/angular-ui/bootstrap/commit/907c851)), closes [#5262](https://github.com/angular-ui/bootstrap/issues/5262) [#5254](https://github.com/angular-ui/bootstrap/issues/5254)
+* **timepicker:** garbage collect parent watchers ([3f809af](https://github.com/angular-ui/bootstrap/commit/3f809af)), closes [#5277](https://github.com/angular-ui/bootstrap/issues/5277)
+* **tooltip:** fix addClass signature ([6a2d91b](https://github.com/angular-ui/bootstrap/commit/6a2d91b)), closes [#5235](https://github.com/angular-ui/bootstrap/issues/5235)
+* **typeahead:** add guard for destroyed $scope ([e105e85](https://github.com/angular-ui/bootstrap/commit/e105e85)), closes [#5267](https://github.com/angular-ui/bootstrap/issues/5267)
+* **typeahead:** fix css selector ([00ac93e](https://github.com/angular-ui/bootstrap/commit/00ac93e)), closes [#5251](https://github.com/angular-ui/bootstrap/issues/5251)
+
+## Features
+
+* **collapse:** add callback hooks ([446364a](https://github.com/angular-ui/bootstrap/commit/446364a)), closes [#5194](https://github.com/angular-ui/bootstrap/issues/5194) [#5226](https://github.com/angular-ui/bootstrap/issues/5226)
+* **datepicker:** extract inline styles to stylesheet ([6843ab6](https://github.com/angular-ui/bootstrap/commit/6843ab6)), closes [#5215](https://github.com/angular-ui/bootstrap/issues/5215)
+* **datepicker:** use $locale for starting day ([332eefb](https://github.com/angular-ui/bootstrap/commit/332eefb)), closes [#4954](https://github.com/angular-ui/bootstrap/issues/4954) [#5281](https://github.com/angular-ui/bootstrap/issues/5281)
+* **timepicker:** add placeholder for seconds input ([717ea69](https://github.com/angular-ui/bootstrap/commit/717ea69)), closes [#5257](https://github.com/angular-ui/bootstrap/issues/5257)
+* **timepicker:** move inline styles to stylesheet ([b658b03](https://github.com/angular-ui/bootstrap/commit/b658b03)), closes [#5218](https://github.com/angular-ui/bootstrap/issues/5218)
+* **typeahead:** add title to matches ([f523361](https://github.com/angular-ui/bootstrap/commit/f523361)), closes [#5252](https://github.com/angular-ui/bootstrap/issues/5252)
+* **typeahead:** move inline style to stylesheet ([e8201d1](https://github.com/angular-ui/bootstrap/commit/e8201d1)), closes [#5219](https://github.com/angular-ui/bootstrap/issues/5219)
+
+
+## BREAKING CHANGES
+
+* tab: This undoes the prior change to the template using div
+elements. If one wishes to use div elements, one must override the
+template in one's app and provide the necessary CSS
+
+
+
+
+# [1.0.3](https://github.com/angular-ui/bootstrap/compare/1.0.2...1.0.3) (2016-01-12)
+
+
+## Bug Fixes
+
+* **timepicker:** fix injection ([627a451](https://github.com/angular-ui/bootstrap/commit/627a451))
+
+
+
+
+# [1.0.2](https://github.com/angular-ui/bootstrap/compare/1.0.1...1.0.2) (2016-01-12)
+
+
+## Bug Fixes
+
+* **tabs:** fix csp check ([74be568](https://github.com/angular-ui/bootstrap/commit/74be568)), closes [#5214](https://github.com/angular-ui/bootstrap/issues/5214)
+
+
+
+
+# [1.0.1](https://github.com/angular-ui/bootstrap/compare/1.0.0...1.0.1) (2016-01-12)
+
+
+## Bug Fixes
+
+* **build:** add ; after script ([9d0269e](https://github.com/angular-ui/bootstrap/commit/9d0269e)), closes [#5197](https://github.com/angular-ui/bootstrap/issues/5197) [#5196](https://github.com/angular-ui/bootstrap/issues/5196)
+* **carousel:** reset active for buffered transitions ([36fca74](https://github.com/angular-ui/bootstrap/commit/36fca74)), closes [#5195](https://github.com/angular-ui/bootstrap/issues/5195) [#5178](https://github.com/angular-ui/bootstrap/issues/5178)
+* **datepicker:** fix error message ([aa010b0](https://github.com/angular-ui/bootstrap/commit/aa010b0)), closes [#5198](https://github.com/angular-ui/bootstrap/issues/5198) [#5191](https://github.com/angular-ui/bootstrap/issues/5191)
+* **datepicker:** fix usage of non-standard format ([428beaf](https://github.com/angular-ui/bootstrap/commit/428beaf)), closes [#5188](https://github.com/angular-ui/bootstrap/issues/5188) [#5187](https://github.com/angular-ui/bootstrap/issues/5187)
+* **timepicker:** prevent mixture of numbers and letters ([9e9c6cf](https://github.com/angular-ui/bootstrap/commit/9e9c6cf)), closes [#5201](https://github.com/angular-ui/bootstrap/issues/5201) [#5085](https://github.com/angular-ui/bootstrap/issues/5085)
+* **timepicker:** use correct disabled expression ([71afeeb](https://github.com/angular-ui/bootstrap/commit/71afeeb)), closes [#5185](https://github.com/angular-ui/bootstrap/issues/5185) [#5182](https://github.com/angular-ui/bootstrap/issues/5182)
+* **typeahead:** scroll only parent element ([3dac5e3](https://github.com/angular-ui/bootstrap/commit/3dac5e3)), closes [#5212](https://github.com/angular-ui/bootstrap/issues/5212) [#5180](https://github.com/angular-ui/bootstrap/issues/5180)
+
+## Features
+
+* **tabs:** add CSS to css file ([db7adf7](https://github.com/angular-ui/bootstrap/commit/db7adf7)), closes [#5211](https://github.com/angular-ui/bootstrap/issues/5211)
+* **timepicker:** add templateUrl in timepickerConfig ([4b0e381](https://github.com/angular-ui/bootstrap/commit/4b0e381)), closes [#5087](https://github.com/angular-ui/bootstrap/issues/5087) [#5200](https://github.com/angular-ui/bootstrap/issues/5200)
+
+
+
+
+# [1.0.0](https://github.com/angular-ui/bootstrap/compare/0.14.3...1.0.0) (2016-01-08)
+
+
+## Bug Fixes
+
+* **accordion:** ensure panelOpen class is present ([0917623](https://github.com/angular-ui/bootstrap/commit/0917623)), closes [#4849](https://github.com/angular-ui/bootstrap/issues/4849) [#4870](https://github.com/angular-ui/bootstrap/issues/4870)
+* **accordion:** fix unexpected routing ([df211bd](https://github.com/angular-ui/bootstrap/commit/df211bd)), closes [#4792](https://github.com/angular-ui/bootstrap/issues/4792)
+* **carousel:** decouple animation information from DOM ([38c1b14](https://github.com/angular-ui/bootstrap/commit/38c1b14)), closes [#4737](https://github.com/angular-ui/bootstrap/issues/4737) [#4516](https://github.com/angular-ui/bootstrap/issues/4516)
+* **carousel:** fix conditions for animation ([12a37e0](https://github.com/angular-ui/bootstrap/commit/12a37e0)), closes [#4974](https://github.com/angular-ui/bootstrap/issues/4974) [#4972](https://github.com/angular-ui/bootstrap/issues/4972)
+* **carousel:** remove version checks ([9d93af1](https://github.com/angular-ui/bootstrap/commit/9d93af1)), closes [#4122](https://github.com/angular-ui/bootstrap/issues/4122) [#4774](https://github.com/angular-ui/bootstrap/issues/4774)
+* **carousel:** resolve rendering issues ([763cfd9](https://github.com/angular-ui/bootstrap/commit/763cfd9)), closes [#4984](https://github.com/angular-ui/bootstrap/issues/4984) [#4361](https://github.com/angular-ui/bootstrap/issues/4361) [#4823](https://github.com/angular-ui/bootstrap/issues/4823) [#4969](https://github.com/angular-ui/bootstrap/issues/4969)
+* **collapse:** set initial state to avoid animation ([5ad08ff](https://github.com/angular-ui/bootstrap/commit/5ad08ff)), closes [#5075](https://github.com/angular-ui/bootstrap/issues/5075) [#4848](https://github.com/angular-ui/bootstrap/issues/4848) [#4885](https://github.com/angular-ui/bootstrap/issues/4885)
+* **dateparser:** baseDate only care about dates ([21b2297](https://github.com/angular-ui/bootstrap/commit/21b2297)), closes [#4767](https://github.com/angular-ui/bootstrap/issues/4767)
+* **dateparser:** enforce order of regex construction ([83d1435](https://github.com/angular-ui/bootstrap/commit/83d1435)), closes [#4810](https://github.com/angular-ui/bootstrap/issues/4810) [#4808](https://github.com/angular-ui/bootstrap/issues/4808)
+* **datepicker:** active date should be model if present ([9019298](https://github.com/angular-ui/bootstrap/commit/9019298)), closes [#5082](https://github.com/angular-ui/bootstrap/issues/5082) [#5081](https://github.com/angular-ui/bootstrap/issues/5081)
+* **datepicker:** correctly display pre-100 years ([beabb4a](https://github.com/angular-ui/bootstrap/commit/beabb4a)), closes [#4812](https://github.com/angular-ui/bootstrap/issues/4812) [#4032](https://github.com/angular-ui/bootstrap/issues/4032)
+* **datepicker:** correctly set minMode/maxMode ([1524080](https://github.com/angular-ui/bootstrap/commit/1524080)), closes [#5093](https://github.com/angular-ui/bootstrap/issues/5093) [#5090](https://github.com/angular-ui/bootstrap/issues/5090)
+* **datepicker:** fix minDate/maxDate with literals ([e7f709f](https://github.com/angular-ui/bootstrap/commit/e7f709f)), closes [#4841](https://github.com/angular-ui/bootstrap/issues/4841) [#3437](https://github.com/angular-ui/bootstrap/issues/3437)
+* **datepicker:** stop propagation of esc in popup ([000d6c3](https://github.com/angular-ui/bootstrap/commit/000d6c3)), closes [#5074](https://github.com/angular-ui/bootstrap/issues/5074) [#5013](https://github.com/angular-ui/bootstrap/issues/5013)
+* **datepicker:** update with alternative format ([fd88dcb](https://github.com/angular-ui/bootstrap/commit/fd88dcb)), closes [#5014](https://github.com/angular-ui/bootstrap/issues/5014)
+* **debounce:** fix argument slicing ([e196be8](https://github.com/angular-ui/bootstrap/commit/e196be8)), closes [#4859](https://github.com/angular-ui/bootstrap/issues/4859) [#4860](https://github.com/angular-ui/bootstrap/issues/4860)
+* **dropdown:** do not close on right click ([bf1768e](https://github.com/angular-ui/bootstrap/commit/bf1768e)), closes [#5052](https://github.com/angular-ui/bootstrap/issues/5052) [#5051](https://github.com/angular-ui/bootstrap/issues/5051)
+* **dropdown:** remove class support for uib-dropdown-menu directive ([43535cf](https://github.com/angular-ui/bootstrap/commit/43535cf)), closes [#4753](https://github.com/angular-ui/bootstrap/issues/4753)
+* **dropdown:** remove extra uib-keyboard-nav ([57f72b2](https://github.com/angular-ui/bootstrap/commit/57f72b2)), closes [#4891](https://github.com/angular-ui/bootstrap/issues/4891)
+* **modal:** add focus check for IE ([a5c2a5b](https://github.com/angular-ui/bootstrap/commit/a5c2a5b)), closes [#5097](https://github.com/angular-ui/bootstrap/issues/5097) [#5096](https://github.com/angular-ui/bootstrap/issues/5096)
+* **modal:** clean up animation when disabled ([972dee6](https://github.com/angular-ui/bootstrap/commit/972dee6)), closes [#4740](https://github.com/angular-ui/bootstrap/issues/4740) [#4672](https://github.com/angular-ui/bootstrap/issues/4672)
+* **modal:** fix bindToController ([b8969d1](https://github.com/angular-ui/bootstrap/commit/b8969d1)), closes [#5048](https://github.com/angular-ui/bootstrap/issues/5048) [#5039](https://github.com/angular-ui/bootstrap/issues/5039)
+* **modal:** retain focus if child has focus ([726ccc3](https://github.com/angular-ui/bootstrap/commit/726ccc3)), closes [#4904](https://github.com/angular-ui/bootstrap/issues/4904) [#4903](https://github.com/angular-ui/bootstrap/issues/4903)
+* **modal:** trap tabbing regardless of config ([c0f1027](https://github.com/angular-ui/bootstrap/commit/c0f1027)), closes [#5010](https://github.com/angular-ui/bootstrap/issues/5010) [#4990](https://github.com/angular-ui/bootstrap/issues/4990)
+* **pagination:** retain model on initialization ([30099a0](https://github.com/angular-ui/bootstrap/commit/30099a0)), closes [#3786](https://github.com/angular-ui/bootstrap/issues/3786) [#4783](https://github.com/angular-ui/bootstrap/issues/4783) [#2956](https://github.com/angular-ui/bootstrap/issues/2956)
+* **tab:** fix unexpected routing ([d59083b](https://github.com/angular-ui/bootstrap/commit/d59083b)), closes [#4793](https://github.com/angular-ui/bootstrap/issues/4793) [#3266](https://github.com/angular-ui/bootstrap/issues/3266)
+* **timepicker:** correct meridian toggle condition ([09ad740](https://github.com/angular-ui/bootstrap/commit/09ad740)), closes [#4435](https://github.com/angular-ui/bootstrap/issues/4435)
+* **tooltip:** race condition when setting position ([429ddc1](https://github.com/angular-ui/bootstrap/commit/429ddc1)), closes [#4765](https://github.com/angular-ui/bootstrap/issues/4765) [#4757](https://github.com/angular-ui/bootstrap/issues/4757)
+* **typeahead:** allow parent to be required ([3aa41f0](https://github.com/angular-ui/bootstrap/commit/3aa41f0)), closes [#4800](https://github.com/angular-ui/bootstrap/issues/4800) [#2679](https://github.com/angular-ui/bootstrap/issues/2679)
+* **typeahead:** clear typeahead input when editable is false ([1d9294c](https://github.com/angular-ui/bootstrap/commit/1d9294c)), closes [#1620](https://github.com/angular-ui/bootstrap/issues/1620) [#4265](https://github.com/angular-ui/bootstrap/issues/4265) [#4752](https://github.com/angular-ui/bootstrap/issues/4752)
+* **typeahead:** fix programmatic focus issue ([cab0945](https://github.com/angular-ui/bootstrap/commit/cab0945)), closes [#5150](https://github.com/angular-ui/bootstrap/issues/5150) [#764](https://github.com/angular-ui/bootstrap/issues/764)
+* **typeahead:** use correct selector ([e1e6e1b](https://github.com/angular-ui/bootstrap/commit/e1e6e1b)), closes [#5168](https://github.com/angular-ui/bootstrap/issues/5168) [#5167](https://github.com/angular-ui/bootstrap/issues/5167)
+* allow library to be loaded async ([a851636](https://github.com/angular-ui/bootstrap/commit/a851636)), closes [#4775](https://github.com/angular-ui/bootstrap/issues/4775) [#3665](https://github.com/angular-ui/bootstrap/issues/3665)
+
+## Features
+
+* **accordion:** remove deprecated code ([0010aff](https://github.com/angular-ui/bootstrap/commit/0010aff)), closes [#4706](https://github.com/angular-ui/bootstrap/issues/4706)
+* **alert:** remove deprecated code ([21e852b](https://github.com/angular-ui/bootstrap/commit/21e852b)), closes [#4714](https://github.com/angular-ui/bootstrap/issues/4714)
+* **buttons:** add uib-uncheckable support ([b77618e](https://github.com/angular-ui/bootstrap/commit/b77618e)), closes [#3604](https://github.com/angular-ui/bootstrap/issues/3604) [#4791](https://github.com/angular-ui/bootstrap/issues/4791)
+* **buttons:** remove deprecated code ([b549263](https://github.com/angular-ui/bootstrap/commit/b549263)), closes [#4716](https://github.com/angular-ui/bootstrap/issues/4716)
+* **carousel:** expose next and prev on controller ([9b80ee1](https://github.com/angular-ui/bootstrap/commit/9b80ee1)), closes [#4851](https://github.com/angular-ui/bootstrap/issues/4851) [#4853](https://github.com/angular-ui/bootstrap/issues/4853)
+* **carousel:** remove deprecated code ([b159b21](https://github.com/angular-ui/bootstrap/commit/b159b21)), closes [#4717](https://github.com/angular-ui/bootstrap/issues/4717)
+* **collapse:** remove deprecated code ([bc004df](https://github.com/angular-ui/bootstrap/commit/bc004df)), closes [#4715](https://github.com/angular-ui/bootstrap/issues/4715)
+* **dateparser:** add M! and d! support ([b1cfc57](https://github.com/angular-ui/bootstrap/commit/b1cfc57)), closes [#4805](https://github.com/angular-ui/bootstrap/issues/4805) [#4809](https://github.com/angular-ui/bootstrap/issues/4809)
+* **dateparser:** add new format support ([ea388b3](https://github.com/angular-ui/bootstrap/commit/ea388b3)), closes [#3418](https://github.com/angular-ui/bootstrap/issues/3418) [#4833](https://github.com/angular-ui/bootstrap/issues/4833)
+* **dateparser:** add support for literals ([1c79888](https://github.com/angular-ui/bootstrap/commit/1c79888)), closes [#3914](https://github.com/angular-ui/bootstrap/issues/3914) [#4426](https://github.com/angular-ui/bootstrap/issues/4426)
+* **dateparser:** add Z support ([2fb812b](https://github.com/angular-ui/bootstrap/commit/2fb812b)), closes [#4831](https://github.com/angular-ui/bootstrap/issues/4831)
+* **dateparser:** change template literal from ' to ` ([9513f10](https://github.com/angular-ui/bootstrap/commit/9513f10)), closes [#4880](https://github.com/angular-ui/bootstrap/issues/4880) [#4936](https://github.com/angular-ui/bootstrap/issues/4936) [#4938](https://github.com/angular-ui/bootstrap/issues/4938)
+* **dateparser:** remove deprecated code ([2d68f41](https://github.com/angular-ui/bootstrap/commit/2d68f41)), closes [#4718](https://github.com/angular-ui/bootstrap/issues/4718)
+* **datepicker:** add allowInvalid support ([2460e42](https://github.com/angular-ui/bootstrap/commit/2460e42)), closes [#4694](https://github.com/angular-ui/bootstrap/issues/4694) [#4837](https://github.com/angular-ui/bootstrap/issues/4837)
+* **datepicker:** add disabled and ngDisabled support ([434c602](https://github.com/angular-ui/bootstrap/commit/434c602)), closes [#4059](https://github.com/angular-ui/bootstrap/issues/4059) [#4814](https://github.com/angular-ui/bootstrap/issues/4814)
+* **datepicker:** add semantic classes ([97c4333](https://github.com/angular-ui/bootstrap/commit/97c4333)), closes [#4761](https://github.com/angular-ui/bootstrap/issues/4761)
+* **datepicker:** add timezone support ([09098f8](https://github.com/angular-ui/bootstrap/commit/09098f8)), closes [#5062](https://github.com/angular-ui/bootstrap/issues/5062)
+* **datepicker:** implements alternative format support ([8bfeda0](https://github.com/angular-ui/bootstrap/commit/8bfeda0)), closes [#4951](https://github.com/angular-ui/bootstrap/issues/4951) [#4952](https://github.com/angular-ui/bootstrap/issues/4952)
+* **datepicker:** pass through attrs in popup ([26d3103](https://github.com/angular-ui/bootstrap/commit/26d3103)), closes [#4863](https://github.com/angular-ui/bootstrap/issues/4863) [#3338](https://github.com/angular-ui/bootstrap/issues/3338)
+* **datepicker:** remove deprecated code ([2fc3f21](https://github.com/angular-ui/bootstrap/commit/2fc3f21)), closes [#4708](https://github.com/angular-ui/bootstrap/issues/4708)
+* **datepicker:** yearRange -> yearRows and yearColumns ([b784422](https://github.com/angular-ui/bootstrap/commit/b784422)), closes [#3348](https://github.com/angular-ui/bootstrap/issues/3348) [#4970](https://github.com/angular-ui/bootstrap/issues/4970)
+* **dropdown:** add `append-to` support ([809ecdb](https://github.com/angular-ui/bootstrap/commit/809ecdb)), closes [#4467](https://github.com/angular-ui/bootstrap/issues/4467) [#4488](https://github.com/angular-ui/bootstrap/issues/4488)
+* **dropdown:** add open class support ([0495ff0](https://github.com/angular-ui/bootstrap/commit/0495ff0)), closes [#4466](https://github.com/angular-ui/bootstrap/issues/4466) [#4794](https://github.com/angular-ui/bootstrap/issues/4794)
+* **dropdown:** remove deprecated code ([ca3a343](https://github.com/angular-ui/bootstrap/commit/ca3a343)), closes [#4719](https://github.com/angular-ui/bootstrap/issues/4719)
+* **modal:** add appendTo support ([16d854c](https://github.com/angular-ui/bootstrap/commit/16d854c)), closes [#4599](https://github.com/angular-ui/bootstrap/issues/4599)
+* **modal:** add closed promise ([e9c4977](https://github.com/angular-ui/bootstrap/commit/e9c4977)), closes [#4979](https://github.com/angular-ui/bootstrap/issues/4979)
+* **modal:** add pluggable resolve support ([2635f0d](https://github.com/angular-ui/bootstrap/commit/2635f0d)), closes [#3405](https://github.com/angular-ui/bootstrap/issues/3405) [#5078](https://github.com/angular-ui/bootstrap/issues/5078)
+* **modal:** allow appending outside iframe ([80df015](https://github.com/angular-ui/bootstrap/commit/80df015)), closes [#4818](https://github.com/angular-ui/bootstrap/issues/4818)
+* **modal:** change to use $animate ([742132a](https://github.com/angular-ui/bootstrap/commit/742132a)), closes [#3418](https://github.com/angular-ui/bootstrap/issues/3418) [#4834](https://github.com/angular-ui/bootstrap/issues/4834)
+* **modal:** remove deprecated code ([a85d499](https://github.com/angular-ui/bootstrap/commit/a85d499)), closes [#4709](https://github.com/angular-ui/bootstrap/issues/4709)
+* **modal:** support requiring from parent directive ([e28cced](https://github.com/angular-ui/bootstrap/commit/e28cced)), closes [#3765](https://github.com/angular-ui/bootstrap/issues/3765) [#4844](https://github.com/angular-ui/bootstrap/issues/4844)
+* **pager:** change controllerAs to pager ([5890248](https://github.com/angular-ui/bootstrap/commit/5890248)), closes [#4961](https://github.com/angular-ui/bootstrap/issues/4961)
+* **pager:** move to separate component ([2a3314d](https://github.com/angular-ui/bootstrap/commit/2a3314d)), closes [#4935](https://github.com/angular-ui/bootstrap/issues/4935)
+* **pagination:** add force-ellipses option and boundaryLinkNumbers ([56642ea](https://github.com/angular-ui/bootstrap/commit/56642ea)), closes [#2924](https://github.com/angular-ui/bootstrap/issues/2924) [#3064](https://github.com/angular-ui/bootstrap/issues/3064) [#3565](https://github.com/angular-ui/bootstrap/issues/3565)
+* **pagination:** remove deprecated code ([75e493a](https://github.com/angular-ui/bootstrap/commit/75e493a)), closes [#4720](https://github.com/angular-ui/bootstrap/issues/4720)
+* **pagination:** Show ellipsis when rotating ([3f307e4](https://github.com/angular-ui/bootstrap/commit/3f307e4))
+* **paging:** factor out common controller code ([f2f8c4e](https://github.com/angular-ui/bootstrap/commit/f2f8c4e)), closes [#4803](https://github.com/angular-ui/bootstrap/issues/4803) [#4968](https://github.com/angular-ui/bootstrap/issues/4968)
+* **position:** implement auto positioning ([d265113](https://github.com/angular-ui/bootstrap/commit/d265113))
+* **position:** remove deprecated code ([42fa28f](https://github.com/angular-ui/bootstrap/commit/42fa28f)), closes [#4721](https://github.com/angular-ui/bootstrap/issues/4721)
+* **progressbar:** remove deprecated code ([0669b06](https://github.com/angular-ui/bootstrap/commit/0669b06)), closes [#4722](https://github.com/angular-ui/bootstrap/issues/4722)
+* **rating:** remove deprecated code ([d844623](https://github.com/angular-ui/bootstrap/commit/d844623)), closes [#4723](https://github.com/angular-ui/bootstrap/issues/4723)
+* **tabs:** add controllerAs support ([a5cac90](https://github.com/angular-ui/bootstrap/commit/a5cac90)), closes [#5019](https://github.com/angular-ui/bootstrap/issues/5019) [#5020](https://github.com/angular-ui/bootstrap/issues/5020)
+* **tabs:** remove deprecated code ([1b75164](https://github.com/angular-ui/bootstrap/commit/1b75164)), closes [#4710](https://github.com/angular-ui/bootstrap/issues/4710)
+* **timepicker:** add model state support ([fe69386](https://github.com/angular-ui/bootstrap/commit/fe69386)), closes [#3527](https://github.com/angular-ui/bootstrap/issues/3527) [#4835](https://github.com/angular-ui/bootstrap/issues/4835)
+* **timepicker:** add ngDisabled support ([4651191](https://github.com/angular-ui/bootstrap/commit/4651191)), closes [#2219](https://github.com/angular-ui/bootstrap/issues/2219) [#4811](https://github.com/angular-ui/bootstrap/issues/4811)
+* **timepicker:** add semantic classes ([1a822a1](https://github.com/angular-ui/bootstrap/commit/1a822a1)), closes [#4764](https://github.com/angular-ui/bootstrap/issues/4764) [#4971](https://github.com/angular-ui/bootstrap/issues/4971)
+* **timepicker:** add support for seconds ([c7fa845](https://github.com/angular-ui/bootstrap/commit/c7fa845)), closes [#4768](https://github.com/angular-ui/bootstrap/issues/4768)
+* **timepicker:** added ability to handle empty model ([8ffdaeb](https://github.com/angular-ui/bootstrap/commit/8ffdaeb)), closes [#1114](https://github.com/angular-ui/bootstrap/issues/1114) [#4203](https://github.com/angular-ui/bootstrap/issues/4203) [#4617](https://github.com/angular-ui/bootstrap/issues/4617)
+* **timepicker:** remove deprecated code ([feb2b73](https://github.com/angular-ui/bootstrap/commit/feb2b73)), closes [#4712](https://github.com/angular-ui/bootstrap/issues/4712)
+* **tooltip:** add appendToBody only attribute support ([2a1aaf2](https://github.com/angular-ui/bootstrap/commit/2a1aaf2)), closes [#4945](https://github.com/angular-ui/bootstrap/issues/4945) [#5071](https://github.com/angular-ui/bootstrap/issues/5071)
+* **tooltip:** add outsideClick trigger ([8737303](https://github.com/angular-ui/bootstrap/commit/8737303)), closes [#4419](https://github.com/angular-ui/bootstrap/issues/4419) [#4871](https://github.com/angular-ui/bootstrap/issues/4871)
+* **tooltip:** change back to jqLite listeners ([a5ca78a](https://github.com/angular-ui/bootstrap/commit/a5ca78a)), closes [#4886](https://github.com/angular-ui/bootstrap/issues/4886) [#5157](https://github.com/angular-ui/bootstrap/issues/5157)
+* **tooltip:** remove deprecated code ([187f64c](https://github.com/angular-ui/bootstrap/commit/187f64c)), closes [#4713](https://github.com/angular-ui/bootstrap/issues/4713)
+* **typeahead:** change to `appendTo` ([8637afc](https://github.com/angular-ui/bootstrap/commit/8637afc)), closes [#4797](https://github.com/angular-ui/bootstrap/issues/4797)
+* add npm support in main repository ([a9e476f](https://github.com/angular-ui/bootstrap/commit/a9e476f)), closes [#4739](https://github.com/angular-ui/bootstrap/issues/4739) [#5129](https://github.com/angular-ui/bootstrap/issues/5129)
+* prefix virtual templates with `uib/` ([342c576](https://github.com/angular-ui/bootstrap/commit/342c576)), closes [#1205](https://github.com/angular-ui/bootstrap/issues/1205) [#4839](https://github.com/angular-ui/bootstrap/issues/4839)
+* **typeahead:** add 'is-open' support ([167cfad](https://github.com/angular-ui/bootstrap/commit/167cfad)), closes [#4760](https://github.com/angular-ui/bootstrap/issues/4760) [#4779](https://github.com/angular-ui/bootstrap/issues/4779)
+* **typeahead:** add ability to scroll with matches ([a1355e7](https://github.com/angular-ui/bootstrap/commit/a1355e7)), closes [#4463](https://github.com/angular-ui/bootstrap/issues/4463)
+* **typeahead:** add dynamic toggling of Editable ([a5bafe6](https://github.com/angular-ui/bootstrap/commit/a5bafe6)), closes [#2638](https://github.com/angular-ui/bootstrap/issues/2638) [#4820](https://github.com/angular-ui/bootstrap/issues/4820)
+* **typeahead:** add event object to onSelect ([3e876b8](https://github.com/angular-ui/bootstrap/commit/3e876b8)), closes [#5165](https://github.com/angular-ui/bootstrap/issues/5165)
+* **typeahead:** add min-length === 0 support ([d859f42](https://github.com/angular-ui/bootstrap/commit/d859f42)), closes [#764](https://github.com/angular-ui/bootstrap/issues/764) [#2324](https://github.com/angular-ui/bootstrap/issues/2324) [#4789](https://github.com/angular-ui/bootstrap/issues/4789)
+* **typeahead:** add ng-model-options debounce support ([bd47f6c](https://github.com/angular-ui/bootstrap/commit/bd47f6c)), closes [#4982](https://github.com/angular-ui/bootstrap/issues/4982)
+* **typeahead:** add show-hint option ([ef82ad1](https://github.com/angular-ui/bootstrap/commit/ef82ad1)), closes [#2570](https://github.com/angular-ui/bootstrap/issues/2570) [#4784](https://github.com/angular-ui/bootstrap/issues/4784)
+* **typeahead:** remove deprecated code ([606d419](https://github.com/angular-ui/bootstrap/commit/606d419)), closes [#4711](https://github.com/angular-ui/bootstrap/issues/4711)
+
+## Reverts
+
+* **dateparser:** change template literal to ' ([f40066a](https://github.com/angular-ui/bootstrap/commit/f40066a)), closes [#5091](https://github.com/angular-ui/bootstrap/issues/5091)
+* **progressbar:** remove min-width ([ed7e460](https://github.com/angular-ui/bootstrap/commit/ed7e460)), closes [#5141](https://github.com/angular-ui/bootstrap/issues/5141)
+
+
+## BREAKING CHANGES
+
+* all: All of the deprecated services/directives/etc. are removed - one must use all prefixed versions instead
+* pager: As part of the split of the pager component from the
+pagination component, this changes the controllerAs use to `pager` from
+`pagination`
+* dropdown: `keyboard-nav` for the dropdown is no longer a directive and to use it you have to use `keyboard-nav` instead of `uib-keyboard-nav`.
+* dropdown: remove class support for `uib-dropdown-menu` directive.
+* All virtual templates in UI Bootstrap now are prefixed
+with `uib/` - if one is overriding the templates via `$templateCache` path
+or manually building the templates from the UI Bootstrap repository, one
+needs to change the string used in the `$templateCache` representation
+to have the new prefix
+* typeahead: Usage before
+```html
+
+
+```
+After
+```html
+
+
+```
+```js
+$scope.typeaheadContainer = angular.element(document.querySelector('#typeaheadContainer'));
+```
+* tab: This causes the cursor style to be removed from the tab - implement CSS on the `.uib-tab > div` selector, or similar, accordingly
+* accordion: This causes the cursor style to be removed from the heading - implement CSS on the `accordion-toggle` class accordingly
+* datepicker: yearRange is replaced by yearRows and yearColumns for manually specifying the dimensions of the yearpicker. If one wants the prior behavior with yearRange with a different number of rows, just set yearRows
+
+
+
+
+# [0.14.3](https://github.com/angular-ui/bootstrap/compare/0.14.2...0.14.3) (2015-10-23)
+
+
+## Bug Fixes
+
+* **alert:** allow interpolations with dismiss-on-timeout ([de24f46](https://github.com/angular-ui/bootstrap/commit/de24f46)), closes [#4665](https://github.com/angular-ui/bootstrap/issues/4665) [#4666](https://github.com/angular-ui/bootstrap/issues/4666)
+* **buttons:** double toggle on spacebar ([e8808d3](https://github.com/angular-ui/bootstrap/commit/e8808d3)), closes [#4474](https://github.com/angular-ui/bootstrap/issues/4474) [#4630](https://github.com/angular-ui/bootstrap/issues/4630)
+* **collapse:** fix collapse animation timing ([6d1cd0f](https://github.com/angular-ui/bootstrap/commit/6d1cd0f)), closes [#4493](https://github.com/angular-ui/bootstrap/issues/4493)
+* **collapse:** trigger digest after ([3144633](https://github.com/angular-ui/bootstrap/commit/3144633)), closes [#4647](https://github.com/angular-ui/bootstrap/issues/4647) [#4628](https://github.com/angular-ui/bootstrap/issues/4628) [#4561](https://github.com/angular-ui/bootstrap/issues/4561) [#4651](https://github.com/angular-ui/bootstrap/issues/4651)
+* **datepicker:** datepicker-popup nest in dropdown ([134086a](https://github.com/angular-ui/bootstrap/commit/134086a)), closes [#4197](https://github.com/angular-ui/bootstrap/issues/4197) [#4693](https://github.com/angular-ui/bootstrap/issues/4693)
+* **datepicker:** fix support for literal format on popup ([7c3c631](https://github.com/angular-ui/bootstrap/commit/7c3c631)), closes [#4635](https://github.com/angular-ui/bootstrap/issues/4635) [#4616](https://github.com/angular-ui/bootstrap/issues/4616)
+* **tooltip:** delay timeouts ([02425b8](https://github.com/angular-ui/bootstrap/commit/02425b8)), closes [#4621](https://github.com/angular-ui/bootstrap/issues/4621) [#4618](https://github.com/angular-ui/bootstrap/issues/4618)
+* **tooltip:** null scope check in isOpen watch ([1f94104](https://github.com/angular-ui/bootstrap/commit/1f94104)), closes [#4697](https://github.com/angular-ui/bootstrap/issues/4697) [#4683](https://github.com/angular-ui/bootstrap/issues/4683)
+* **tooltip:** scrollbar flashing ([6c82b2b](https://github.com/angular-ui/bootstrap/commit/6c82b2b)), closes [#4550](https://github.com/angular-ui/bootstrap/issues/4550) [#4623](https://github.com/angular-ui/bootstrap/issues/4623) [#4458](https://github.com/angular-ui/bootstrap/issues/4458)
+* **typeahead:** dangling event listeners ([94fb282](https://github.com/angular-ui/bootstrap/commit/94fb282)), closes [#4632](https://github.com/angular-ui/bootstrap/issues/4632) [#4636](https://github.com/angular-ui/bootstrap/issues/4636)
+
+## Features
+
+* **datepicker:** add templateUrl support for pickers ([1f65d87](https://github.com/angular-ui/bootstrap/commit/1f65d87)), closes [#4432](https://github.com/angular-ui/bootstrap/issues/4432)
+* **datepicker:** preserve timezone with model ([0d64aad](https://github.com/angular-ui/bootstrap/commit/0d64aad)), closes [#4676](https://github.com/angular-ui/bootstrap/issues/4676)
+* **modal:** support $uibModalInstance ([97fd37e](https://github.com/angular-ui/bootstrap/commit/97fd37e)), closes [#4638](https://github.com/angular-ui/bootstrap/issues/4638) [#4661](https://github.com/angular-ui/bootstrap/issues/4661)
+
+
+
+
+# [0.14.2](https://github.com/angular-ui/bootstrap/compare/0.14.1...0.14.2) (2015-10-14)
+
+
+## Bug Fixes
+
+* **progressbar:** fix percentage calculation ([feb689c](https://github.com/angular-ui/bootstrap/commit/feb689c)), closes [#4471](https://github.com/angular-ui/bootstrap/issues/4471) [#4588](https://github.com/angular-ui/bootstrap/issues/4588) [#4452](https://github.com/angular-ui/bootstrap/issues/4452)
+* **tooltip:** clean up stackedMap on scope destroy ([ebb5e18](https://github.com/angular-ui/bootstrap/commit/ebb5e18)), closes [#4610](https://github.com/angular-ui/bootstrap/issues/4610) [#4604](https://github.com/angular-ui/bootstrap/issues/4604)
+* **tooltip:** popup close delay not respected ([6daf871](https://github.com/angular-ui/bootstrap/commit/6daf871)), closes [#4597](https://github.com/angular-ui/bootstrap/issues/4597) [#4567](https://github.com/angular-ui/bootstrap/issues/4567)
+
+
+
+
+# [0.14.1](https://github.com/angular-ui/bootstrap/compare/0.14.0...0.14.1) (2015-10-11)
+
+
+## Bug Fixes
+
+* **accordion:** make deprecated controller work with 1.3.x ([c5e6042](https://github.com/angular-ui/bootstrap/commit/c5e6042)), closes [#4574](https://github.com/angular-ui/bootstrap/issues/4574)
+* **alert:** make deprecated controller work with 1.3.x ([e8c8ee6](https://github.com/angular-ui/bootstrap/commit/e8c8ee6)), closes [#4576](https://github.com/angular-ui/bootstrap/issues/4576)
+* **buttons:** make deprecated controller work with 1.3.x ([1e3cbd8](https://github.com/angular-ui/bootstrap/commit/1e3cbd8)), closes [#4577](https://github.com/angular-ui/bootstrap/issues/4577)
+* **carousel:** make deprecated controller work with 1.3.x ([f6c7931](https://github.com/angular-ui/bootstrap/commit/f6c7931)), closes [#4578](https://github.com/angular-ui/bootstrap/issues/4578)
+* **datepicker:** make deprecated controller work with 1.3.x ([18371ab](https://github.com/angular-ui/bootstrap/commit/18371ab)), closes [#4586](https://github.com/angular-ui/bootstrap/issues/4586)
+* **dropdown:** make deprecated controller work with 1.3.x ([ae1a87c](https://github.com/angular-ui/bootstrap/commit/ae1a87c)), closes [#4585](https://github.com/angular-ui/bootstrap/issues/4585)
+* **pagination:** make deprecated controller work with 1.3.x ([d50e8d2](https://github.com/angular-ui/bootstrap/commit/d50e8d2)), closes [#4580](https://github.com/angular-ui/bootstrap/issues/4580)
+* **progressbar:** make deprecated controller work with 1.3.x ([1c5e479](https://github.com/angular-ui/bootstrap/commit/1c5e479)), closes [#4581](https://github.com/angular-ui/bootstrap/issues/4581)
+* **rating:** make deprecated controller work with 1.3.x ([ce1114a](https://github.com/angular-ui/bootstrap/commit/ce1114a)), closes [#4582](https://github.com/angular-ui/bootstrap/issues/4582)
+* **tabs:** make deprecated controller work with 1.3.x ([685bd6a](https://github.com/angular-ui/bootstrap/commit/685bd6a)), closes [#4583](https://github.com/angular-ui/bootstrap/issues/4583)
+* **timepicker:** make deprecated controller work with 1.3.x ([00f60ee](https://github.com/angular-ui/bootstrap/commit/00f60ee)), closes [#4584](https://github.com/angular-ui/bootstrap/issues/4584)
+
+## Features
+
+* **timepicker:** add accessibility improvements ([4ebecbc](https://github.com/angular-ui/bootstrap/commit/4ebecbc)), closes [#4569](https://github.com/angular-ui/bootstrap/issues/4569) [#4573](https://github.com/angular-ui/bootstrap/issues/4573)
+
+
+
+
+# [0.14.0](https://github.com/angular-ui/bootstrap/compare/0.13.4...0.14.0) (2015-10-09)
+
+
+## Bug Fixes
+
+* **accordion:** coerce to boolean ([b864aa9](https://github.com/angular-ui/bootstrap/commit/b864aa9)), closes [#4385](https://github.com/angular-ui/bootstrap/issues/4385)
+* **accordion:** re-expose AccordionController ([5382226](https://github.com/angular-ui/bootstrap/commit/5382226)), closes [#4524](https://github.com/angular-ui/bootstrap/issues/4524)
+* **alert:** properly pass $event as local ([eb2366f](https://github.com/angular-ui/bootstrap/commit/eb2366f)), closes [#4386](https://github.com/angular-ui/bootstrap/issues/4386) [#4387](https://github.com/angular-ui/bootstrap/issues/4387)
+* **alert:** re-expose AlertController ([f561aa9](https://github.com/angular-ui/bootstrap/commit/f561aa9)), closes [#4525](https://github.com/angular-ui/bootstrap/issues/4525)
+* **buttons:** re-expose ButtonsController ([c0dbf79](https://github.com/angular-ui/bootstrap/commit/c0dbf79)), closes [#4526](https://github.com/angular-ui/bootstrap/issues/4526)
+* **carousel:** fix reading of `noTransition` ([2e26815](https://github.com/angular-ui/bootstrap/commit/2e26815)), closes [#4325](https://github.com/angular-ui/bootstrap/issues/4325)
+* **carousel:** improve accessibility ([da71159](https://github.com/angular-ui/bootstrap/commit/da71159)), closes [#4478](https://github.com/angular-ui/bootstrap/issues/4478) [#4479](https://github.com/angular-ui/bootstrap/issues/4479)
+* **carousel:** re-enable deprecated directives ([30e8aa7](https://github.com/angular-ui/bootstrap/commit/30e8aa7)), closes [#4527](https://github.com/angular-ui/bootstrap/issues/4527)
+* **carousel:** reset $currentTransition when no slides ([0b3d5bd](https://github.com/angular-ui/bootstrap/commit/0b3d5bd)), closes [#4532](https://github.com/angular-ui/bootstrap/issues/4532) [#4390](https://github.com/angular-ui/bootstrap/issues/4390)
+* **datepicker:** add check for `contains` ([868c0e2](https://github.com/angular-ui/bootstrap/commit/868c0e2)), closes [#4423](https://github.com/angular-ui/bootstrap/issues/4423) [#4411](https://github.com/angular-ui/bootstrap/issues/4411)
+* **datepicker:** add custom class to year picker ([0ad7cb9](https://github.com/angular-ui/bootstrap/commit/0ad7cb9)), closes [#4558](https://github.com/angular-ui/bootstrap/issues/4558) [#4546](https://github.com/angular-ui/bootstrap/issues/4546)
+* **datepicker:** change to `$popup` ([65814f1](https://github.com/angular-ui/bootstrap/commit/65814f1))
+* **datepicker:** datepicker-popup nest in dropdown ([6b4267b](https://github.com/angular-ui/bootstrap/commit/6b4267b)), closes [#4489](https://github.com/angular-ui/bootstrap/issues/4489) [#4197](https://github.com/angular-ui/bootstrap/issues/4197)
+* **datepicker:** remove focus management on date selection by keyboard ([36ecf60](https://github.com/angular-ui/bootstrap/commit/36ecf60)), closes [#4409](https://github.com/angular-ui/bootstrap/issues/4409)
+* **dropdown:** ensure class is present in dropdown-menu ([92ab48e](https://github.com/angular-ui/bootstrap/commit/92ab48e)), closes [#4523](https://github.com/angular-ui/bootstrap/issues/4523) [#4442](https://github.com/angular-ui/bootstrap/issues/4442)
+* **dropdown:** restore deprecated directives ([e7c5879](https://github.com/angular-ui/bootstrap/commit/e7c5879)), closes [#4514](https://github.com/angular-ui/bootstrap/issues/4514)
+* **modal:** fix for conflicts with ngTouch module on mobile devices ([508aceb](https://github.com/angular-ui/bootstrap/commit/508aceb)), closes [#2280](https://github.com/angular-ui/bootstrap/issues/2280) [#4357](https://github.com/angular-ui/bootstrap/issues/4357)
+* **progressbar:** re-expose ProgressController ([5604e59](https://github.com/angular-ui/bootstrap/commit/5604e59)), closes [#4528](https://github.com/angular-ui/bootstrap/issues/4528)
+* **rating:** re-expose RatingController ([aede646](https://github.com/angular-ui/bootstrap/commit/aede646)), closes [#4529](https://github.com/angular-ui/bootstrap/issues/4529)
+* **tabs:** re-expose TabsetController ([435924f](https://github.com/angular-ui/bootstrap/commit/435924f)), closes [#4530](https://github.com/angular-ui/bootstrap/issues/4530)
+* **timepicker:** re-expose TimepickerController ([3aa9841](https://github.com/angular-ui/bootstrap/commit/3aa9841)), closes [#4531](https://github.com/angular-ui/bootstrap/issues/4531)
+* **tooltip:** add display block to style ([b413a22](https://github.com/angular-ui/bootstrap/commit/b413a22)), closes [#4363](https://github.com/angular-ui/bootstrap/issues/4363) [#4379](https://github.com/angular-ui/bootstrap/issues/4379)
+* **tooltip:** check for ttScope in $$postDigest ([01b9624](https://github.com/angular-ui/bootstrap/commit/01b9624)), closes [#4555](https://github.com/angular-ui/bootstrap/issues/4555) [#4552](https://github.com/angular-ui/bootstrap/issues/4552)
+* **tooltip:** correct flash of reposition ([8fee75d](https://github.com/angular-ui/bootstrap/commit/8fee75d)), closes [#4363](https://github.com/angular-ui/bootstrap/issues/4363) [#4195](https://github.com/angular-ui/bootstrap/issues/4195)
+* **tooltip:** do nothing if `$scope` doesn't exist ([1e039e8](https://github.com/angular-ui/bootstrap/commit/1e039e8)), closes [#4346](https://github.com/angular-ui/bootstrap/issues/4346) [#3347](https://github.com/angular-ui/bootstrap/issues/3347)
+* **tooltip:** fix binding to multiple triggers ([d6cda93](https://github.com/angular-ui/bootstrap/commit/d6cda93)), closes [#4371](https://github.com/angular-ui/bootstrap/issues/4371) [#4384](https://github.com/angular-ui/bootstrap/issues/4384)
+* **tooltip:** isOpen to work with expressions ([5f68280](https://github.com/angular-ui/bootstrap/commit/5f68280)), closes [#4380](https://github.com/angular-ui/bootstrap/issues/4380) [#4362](https://github.com/angular-ui/bootstrap/issues/4362)
+* **tooltip:** properly gc popupTimeout ([ff52f52](https://github.com/angular-ui/bootstrap/commit/ff52f52)), closes [#2786](https://github.com/angular-ui/bootstrap/issues/2786)
+* **tooltip:** set `visibility: hidden` to avoid flicker ([f7cb8bc](https://github.com/angular-ui/bootstrap/commit/f7cb8bc)), closes [#4342](https://github.com/angular-ui/bootstrap/issues/4342)
+
+## Features
+
+* **accordion:** use uib- prefix ([0328a76](https://github.com/angular-ui/bootstrap/commit/0328a76)), closes [#4389](https://github.com/angular-ui/bootstrap/issues/4389)
+* **accordion:** use uib- prefix ([298ec8c](https://github.com/angular-ui/bootstrap/commit/298ec8c)), closes [#4503](https://github.com/angular-ui/bootstrap/issues/4503)
+* **alert:** use uib- prefix ([5e3a87a](https://github.com/angular-ui/bootstrap/commit/5e3a87a)), closes [#4406](https://github.com/angular-ui/bootstrap/issues/4406)
+* **buttons:** use uib- prefix ([5a1c2c9](https://github.com/angular-ui/bootstrap/commit/5a1c2c9)), closes [#4445](https://github.com/angular-ui/bootstrap/issues/4445)
+* **carousel:** use uib- prefix ([2e5bfac](https://github.com/angular-ui/bootstrap/commit/2e5bfac)), closes [#4501](https://github.com/angular-ui/bootstrap/issues/4501)
+* **collapse:** convert to use `$animateCss` ([533a9f0](https://github.com/angular-ui/bootstrap/commit/533a9f0)), closes [#4257](https://github.com/angular-ui/bootstrap/issues/4257)
+* **collapse:** use uib- prefix ([9bdb32e](https://github.com/angular-ui/bootstrap/commit/9bdb32e)), closes [#4370](https://github.com/angular-ui/bootstrap/issues/4370)
+* **dateparser:** reset parsers when $locale.id changes ([d9a521a](https://github.com/angular-ui/bootstrap/commit/d9a521a)), closes [#4286](https://github.com/angular-ui/bootstrap/issues/4286) [#4425](https://github.com/angular-ui/bootstrap/issues/4425)
+* **dateparser:** use uib- prefix ([0fa851f](https://github.com/angular-ui/bootstrap/commit/0fa851f)), closes [#4504](https://github.com/angular-ui/bootstrap/issues/4504)
+* **datepicker:** add uib- prefix ([44354f6](https://github.com/angular-ui/bootstrap/commit/44354f6)), closes [#4509](https://github.com/angular-ui/bootstrap/issues/4509)
+* **dropdown:** uib- prefix ([5bc0851](https://github.com/angular-ui/bootstrap/commit/5bc0851)), closes [#4510](https://github.com/angular-ui/bootstrap/issues/4510)
+* **modal:** Added ability to add CSS class to top window ([bd38e8f](https://github.com/angular-ui/bootstrap/commit/bd38e8f)), closes [#2524](https://github.com/angular-ui/bootstrap/issues/2524)
+* **modal:** add uib- prefix ([8c7b9e4](https://github.com/angular-ui/bootstrap/commit/8c7b9e4)), closes [#4511](https://github.com/angular-ui/bootstrap/issues/4511)
+* **pagination:** add uib- prefix ([9aea856](https://github.com/angular-ui/bootstrap/commit/9aea856)), closes [#4536](https://github.com/angular-ui/bootstrap/issues/4536)
+* **position:** add uib- prefix ([6158091](https://github.com/angular-ui/bootstrap/commit/6158091)), closes [#4507](https://github.com/angular-ui/bootstrap/issues/4507)
+* **progressbar:** add `aria-labelledby` support ([e6f3b87](https://github.com/angular-ui/bootstrap/commit/e6f3b87)), closes [#4350](https://github.com/angular-ui/bootstrap/issues/4350) [#4347](https://github.com/angular-ui/bootstrap/issues/4347)
+* **rating:** add `aria-valuetext` attribute ([72de2d8](https://github.com/angular-ui/bootstrap/commit/72de2d8)), closes [#4349](https://github.com/angular-ui/bootstrap/issues/4349) [#4347](https://github.com/angular-ui/bootstrap/issues/4347)
+* **rating:** user uib- prefix ([377b4b7](https://github.com/angular-ui/bootstrap/commit/377b4b7)), closes [#4502](https://github.com/angular-ui/bootstrap/issues/4502)
+* **tabs:** use uib- prefix ([d25a8c2](https://github.com/angular-ui/bootstrap/commit/d25a8c2)), closes [#4449](https://github.com/angular-ui/bootstrap/issues/4449)
+* **timepicker:** use uib- prefix ([504e653](https://github.com/angular-ui/bootstrap/commit/504e653)), closes [#4505](https://github.com/angular-ui/bootstrap/issues/4505)
+* **tooltip:** add uib- prefix ([f8bc038](https://github.com/angular-ui/bootstrap/commit/f8bc038)), closes [#4515](https://github.com/angular-ui/bootstrap/issues/4515)
+* **tooltip:** allow custom closing delay ([5f7051b](https://github.com/angular-ui/bootstrap/commit/5f7051b)), closes [#3576](https://github.com/angular-ui/bootstrap/issues/3576)
+* **tooltip:** hide tooltip when `esc` is hit ([c08509a](https://github.com/angular-ui/bootstrap/commit/c08509a)), closes [#4367](https://github.com/angular-ui/bootstrap/issues/4367) [#4248](https://github.com/angular-ui/bootstrap/issues/4248)
+* **typeahead:** add `appendElementToId` ([fdf53e6](https://github.com/angular-ui/bootstrap/commit/fdf53e6)), closes [#4231](https://github.com/angular-ui/bootstrap/issues/4231) [#4497](https://github.com/angular-ui/bootstrap/issues/4497)
+* **typeahead:** add customClass support for dropdown ([fa1cdfc](https://github.com/angular-ui/bootstrap/commit/fa1cdfc)), closes [#4332](https://github.com/angular-ui/bootstrap/issues/4332) [#4410](https://github.com/angular-ui/bootstrap/issues/4410)
+* **typeahead:** add uib- prefix ([9e5e1a2](https://github.com/angular-ui/bootstrap/commit/9e5e1a2)), closes [#4542](https://github.com/angular-ui/bootstrap/issues/4542)
+
+## Reverts
+
+* **dropdown:** undo adding of `open` class on body ([6f9f1fc](https://github.com/angular-ui/bootstrap/commit/6f9f1fc))
+
+
+## BREAKING CHANGES
+
+* Removes focus on datepicker on selection of a date via keyboard for accessibility reasons
+
+
+
+# [0.13.4](https://github.com/angular-ui/bootstrap/compare/0.13.3...0.13.4) (2015-09-03)
+
+
+## Bug Fixes
+
+* **accordion:** add custom open class support ([575eb85](https://github.com/angular-ui/bootstrap/commit/575eb85)), closes [#4198](https://github.com/angular-ui/bootstrap/issues/4198)
+* **datepicker:** ensure the original target is not in popup ([9b2f7ac](https://github.com/angular-ui/bootstrap/commit/9b2f7ac)), closes [#4316](https://github.com/angular-ui/bootstrap/issues/4316) [#4314](https://github.com/angular-ui/bootstrap/issues/4314)
+* **dropdown:** fix display when using with append-to-body ([bf63cef](https://github.com/angular-ui/bootstrap/commit/bf63cef)), closes [#4305](https://github.com/angular-ui/bootstrap/issues/4305) [#4240](https://github.com/angular-ui/bootstrap/issues/4240)
+* **dropdown:** fix up arrow nav support ([defcbbb](https://github.com/angular-ui/bootstrap/commit/defcbbb)), closes [#4330](https://github.com/angular-ui/bootstrap/issues/4330) [#4327](https://github.com/angular-ui/bootstrap/issues/4327)
+* **modal:** Wait for animation before focus. ([937a1f3](https://github.com/angular-ui/bootstrap/commit/937a1f3)), closes [#4300](https://github.com/angular-ui/bootstrap/issues/4300) [#4274](https://github.com/angular-ui/bootstrap/issues/4274)
+* **modal:** correctly remove custom class ([ba2ce24](https://github.com/angular-ui/bootstrap/commit/ba2ce24)), closes [#4175](https://github.com/angular-ui/bootstrap/issues/4175) [#4171](https://github.com/angular-ui/bootstrap/issues/4171)
+* **modal:** fix allowing promises to be resolved ([b1e98b1](https://github.com/angular-ui/bootstrap/commit/b1e98b1)), closes [#4310](https://github.com/angular-ui/bootstrap/issues/4310) [#4309](https://github.com/angular-ui/bootstrap/issues/4309)
+* **progress:** rename to avoid conflict ([07a938d](https://github.com/angular-ui/bootstrap/commit/07a938d)), closes [#4255](https://github.com/angular-ui/bootstrap/issues/4255)
+* **tabs:** ensure tab selection only occurs once ([7d3ba1e](https://github.com/angular-ui/bootstrap/commit/7d3ba1e)), closes [#3060](https://github.com/angular-ui/bootstrap/issues/3060) [#4230](https://github.com/angular-ui/bootstrap/issues/4230) [#2883](https://github.com/angular-ui/bootstrap/issues/2883)
+* **timepicker:** leave view alone if either input is invalid ([818f7e5](https://github.com/angular-ui/bootstrap/commit/818f7e5)), closes [#4160](https://github.com/angular-ui/bootstrap/issues/4160) [#3825](https://github.com/angular-ui/bootstrap/issues/3825)
+* **tooltip:** correctly position tooltip ([457f10c](https://github.com/angular-ui/bootstrap/commit/457f10c)), closes [#4311](https://github.com/angular-ui/bootstrap/issues/4311) [#4195](https://github.com/angular-ui/bootstrap/issues/4195)
+* **tooltip:** fix jshint error ([17cc39f](https://github.com/angular-ui/bootstrap/commit/17cc39f))
+* **tooltip:** properly gc timeout on toggle of disabled ([f8eab55](https://github.com/angular-ui/bootstrap/commit/f8eab55)), closes [#4210](https://github.com/angular-ui/bootstrap/issues/4210) [#4204](https://github.com/angular-ui/bootstrap/issues/4204)
+* **tooltip:** switch to use raw DOM event bindings ([7556bed](https://github.com/angular-ui/bootstrap/commit/7556bed)), closes [#4322](https://github.com/angular-ui/bootstrap/issues/4322) [#4060](https://github.com/angular-ui/bootstrap/issues/4060)
+* **typeahead:** add support for ngModelOptions getterSetter ([ccaa627](https://github.com/angular-ui/bootstrap/commit/ccaa627)), closes [#3865](https://github.com/angular-ui/bootstrap/issues/3865) [#3823](https://github.com/angular-ui/bootstrap/issues/3823)
+* **typeahead:** release references on destruction ([695db9d](https://github.com/angular-ui/bootstrap/commit/695db9d)), closes [#4299](https://github.com/angular-ui/bootstrap/issues/4299) [#4298](https://github.com/angular-ui/bootstrap/issues/4298)
+* **typeahead:** use ng-bind-html ([bb9fa1a](https://github.com/angular-ui/bootstrap/commit/bb9fa1a)), closes [#3463](https://github.com/angular-ui/bootstrap/issues/3463) [#4073](https://github.com/angular-ui/bootstrap/issues/4073)
+
+## Features
+
+* **accordion:** allow custom panel class ([5ee23a4](https://github.com/angular-ui/bootstrap/commit/5ee23a4)), closes [#4242](https://github.com/angular-ui/bootstrap/issues/4242) [#3968](https://github.com/angular-ui/bootstrap/issues/3968)
+* **accordion:** support spacebar to toggle group ([aa5a991](https://github.com/angular-ui/bootstrap/commit/aa5a991)), closes [#4319](https://github.com/angular-ui/bootstrap/issues/4319) [#4249](https://github.com/angular-ui/bootstrap/issues/4249)
+* **buttons:** allow toggling via spacebar when focused ([bdfb289](https://github.com/angular-ui/bootstrap/commit/bdfb289)), closes [#4252](https://github.com/angular-ui/bootstrap/issues/4252) [#4259](https://github.com/angular-ui/bootstrap/issues/4259)
+* **buttons:** hide nested inputs ([a06afe6](https://github.com/angular-ui/bootstrap/commit/a06afe6)), closes [#4282](https://github.com/angular-ui/bootstrap/issues/4282)
+* **carousel:** add model binding support to slide ([dac087e](https://github.com/angular-ui/bootstrap/commit/dac087e)), closes [#4202](https://github.com/angular-ui/bootstrap/issues/4202) [#4201](https://github.com/angular-ui/bootstrap/issues/4201)
+* **dateparser:** add support for the `h` format ([550fe20](https://github.com/angular-ui/bootstrap/commit/550fe20)), closes [#4220](https://github.com/angular-ui/bootstrap/issues/4220)
+* **datepicker:** disable today button if invalid ([71e0b8a](https://github.com/angular-ui/bootstrap/commit/71e0b8a)), closes [#4199](https://github.com/angular-ui/bootstrap/issues/4199) [#3988](https://github.com/angular-ui/bootstrap/issues/3988)
+* **modal:** complete modal open resolution in order ([1bba8b4](https://github.com/angular-ui/bootstrap/commit/1bba8b4)), closes [#2443](https://github.com/angular-ui/bootstrap/issues/2443) [#4302](https://github.com/angular-ui/bootstrap/issues/4302) [#2404](https://github.com/angular-ui/bootstrap/issues/2404)
+* **modal:** support multiple open classes ([3d01c59](https://github.com/angular-ui/bootstrap/commit/3d01c59)), closes [#4226](https://github.com/angular-ui/bootstrap/issues/4226) [#4184](https://github.com/angular-ui/bootstrap/issues/4184)
+* **pagination:** add `ngDisabled` support for the pager ([ba734b4](https://github.com/angular-ui/bootstrap/commit/ba734b4)), closes [#4217](https://github.com/angular-ui/bootstrap/issues/4217) [#2856](https://github.com/angular-ui/bootstrap/issues/2856)
+* **pagination:** add `templateUrl` support ([64b5289](https://github.com/angular-ui/bootstrap/commit/64b5289)), closes [#4162](https://github.com/angular-ui/bootstrap/issues/4162)
+* **tabs:** add support for `x-tab-heading` ([1abfd05](https://github.com/angular-ui/bootstrap/commit/1abfd05)), closes [#4166](https://github.com/angular-ui/bootstrap/issues/4166) [#1893](https://github.com/angular-ui/bootstrap/issues/1893)
+* **timepicker:** add `templateUrl` and `controllerAs` support ([639d511](https://github.com/angular-ui/bootstrap/commit/639d511)), closes [#4275](https://github.com/angular-ui/bootstrap/issues/4275) [#4284](https://github.com/angular-ui/bootstrap/issues/4284)
+* **tooltip:** expose isOpen property ([99b87cc](https://github.com/angular-ui/bootstrap/commit/99b87cc)), closes [#4179](https://github.com/angular-ui/bootstrap/issues/4179) [#2148](https://github.com/angular-ui/bootstrap/issues/2148) [#590](https://github.com/angular-ui/bootstrap/issues/590)
+* **typeahead:** add `typeaheadFocusOnSelect` ([b5ecda3](https://github.com/angular-ui/bootstrap/commit/b5ecda3)), closes [#4212](https://github.com/angular-ui/bootstrap/issues/4212) [#4211](https://github.com/angular-ui/bootstrap/issues/4211) [#4206](https://github.com/angular-ui/bootstrap/issues/4206)
+* **typeahead:** add custom popup template support ([4b02648](https://github.com/angular-ui/bootstrap/commit/4b02648)), closes [#4320](https://github.com/angular-ui/bootstrap/issues/4320) [#3774](https://github.com/angular-ui/bootstrap/issues/3774)
+
+
+## Breaking Changes
+
+* **buttons**
+ * hide nested `` elements on `btn-radio` and `btn-checkbox` directives.
+
+ Fixes #3264
+ Closes #4282
+
+ ([a06afe6](https://github.com/angular-ui/bootstrap/commit/a06afe6))
+
+* **dropdown**
+ * when using `append-to-body`, both the `dropdown` and `open` classes are added to the `` element.
+ * this differs from the existing behavior in that it will no longer toggle based on the existing `dropdown` directive element, but on the `body` element instead.
+
+ Fixes #4240
+ Closes #4305
+
+ ([bf63cef](https://github.com/angular-ui/bootstrap/commit/bf63cef))
+
+* **tooltip**
+ * Switch to use `addEventListener` and `removeEventListener` to prevent jqLite/jQuery bug where the events are swallowed on disabled elements
+ * this affects custom events, which must now be dispatched with `element[0].dispatchEvent(new Event('customEvent'))`, as opposed to `element.trigger('customEvent')`
+
+ Fixes #4060
+ Closes #4322
+
+ ([7556bed](https://github.com/angular-ui/bootstrap/commit/7556beda486f26b40fb860448316e8a32457e9e9))
+
+* **typeahead**
+ * for security reasons, only whitelisted HTML should be added.
+ * the typeahead match template now uses `ng-bind-html` instead of `bind-html-unsafe`.
+ * typeahead now uses the `$sce` service when `ngSanitize` is present and logs a warning if it is not.
+
+ Fixes #2884
+ Closes #3463
+ Closes #4073
+
+ ([bb9fa1a](https://github.com/angular-ui/bootstrap/commit/bb9fa1a))
+
+
+
+# [0.13.3](https://github.com/angular-ui/bootstrap/compare/0.13.2...0.13.3) (2015-08-09)
+
+
+## Bug Fixes
+
+* **accordion:**
+ * add `open` class when expanded ([ead15e37](https://github.com/angular-ui/bootstrap/commit/ead15e37), closes [#4152](https://github.com/angular-ui/bootstrap/issues/4152), [#3419](https://github.com/angular-ui/bootstrap/issues/3419))
+ * revert to empty href ([b18dc8f9](https://github.com/angular-ui/bootstrap/commit/b18dc8f9), closes [#4104](https://github.com/angular-ui/bootstrap/issues/4104))
+* **buttons:**
+ * change to use `attrs.disabled` ([c9b0d0b0](https://github.com/angular-ui/bootstrap/commit/c9b0d0b0), closes [#4088](https://github.com/angular-ui/bootstrap/issues/4088))
+ * allow selection of undisabled button ([707fbf55](https://github.com/angular-ui/bootstrap/commit/707fbf55), closes [#4088](https://github.com/angular-ui/bootstrap/issues/4088))
+* **carousel:**
+ * fix animation direction ([8359d73f](https://github.com/angular-ui/bootstrap/commit/8359d73f), closes [#4092](https://github.com/angular-ui/bootstrap/issues/4092), [#4087](https://github.com/angular-ui/bootstrap/issues/4087))
+ * fix sorting of indicators ([8056368e](https://github.com/angular-ui/bootstrap/commit/8056368e), closes [#4071](https://github.com/angular-ui/bootstrap/issues/4071), [#3764](https://github.com/angular-ui/bootstrap/issues/3764))
+* **dateparser:** Support 12-hour format and AM/PM ([1ecd82ce](https://github.com/angular-ui/bootstrap/commit/1ecd82ce), closes [#4117](https://github.com/angular-ui/bootstrap/issues/4117))
+* **datepicker:**
+ * commit safe apply on destruction ([74a8be4c](https://github.com/angular-ui/bootstrap/commit/74a8be4c), closes [#4079](https://github.com/angular-ui/bootstrap/issues/4079), [#4076](https://github.com/angular-ui/bootstrap/issues/4076))
+ * change to `dateDisabled` ([5245ccad](https://github.com/angular-ui/bootstrap/commit/5245ccad), closes [#2773](https://github.com/angular-ui/bootstrap/issues/2773), [#4080](https://github.com/angular-ui/bootstrap/issues/4080))
+* **dropdown:** handle `keyboard-nav` correctly ([0b37f088](https://github.com/angular-ui/bootstrap/commit/0b37f088), closes [#4110](https://github.com/angular-ui/bootstrap/issues/4110), [#4091](https://github.com/angular-ui/bootstrap/issues/4091))
+* **modal:**
+ * skipping ESC handling for form inputs ([a05b9c1a](https://github.com/angular-ui/bootstrap/commit/a05b9c1a), closes [#3551](https://github.com/angular-ui/bootstrap/issues/3551), [#2544](https://github.com/angular-ui/bootstrap/issues/2544))
+ * add `$animateCss` support ([c7f19d58](https://github.com/angular-ui/bootstrap/commit/c7f19d58), closes [#4121](https://github.com/angular-ui/bootstrap/issues/4121), [#4119](https://github.com/angular-ui/bootstrap/issues/4119))
+ * fix test ([e60c3ff6](https://github.com/angular-ui/bootstrap/commit/e60c3ff6))
+ * dismiss modal on unschedule destruction ([3584061f](https://github.com/angular-ui/bootstrap/commit/3584061f), closes [#4097](https://github.com/angular-ui/bootstrap/issues/4097), [#3694](https://github.com/angular-ui/bootstrap/issues/3694))
+* **progressbar:** fix `min-width` for Bootstrap 3.2 ([8dc13be9](https://github.com/angular-ui/bootstrap/commit/8dc13be9), closes [#4081](https://github.com/angular-ui/bootstrap/issues/4081), [#2511](https://github.com/angular-ui/bootstrap/issues/2511))
+* **tooltip:**
+ * add safety to `$apply` ([22b16f01](https://github.com/angular-ui/bootstrap/commit/22b16f01), closes [#3943](https://github.com/angular-ui/bootstrap/issues/3943), [#4150](https://github.com/angular-ui/bootstrap/issues/4150), [#516](https://github.com/angular-ui/bootstrap/issues/516))
+ * tooltip w/ template position ([895a2281](https://github.com/angular-ui/bootstrap/commit/895a2281))
+ * prevent opening when `tooltipPopupDelay` is present ([12c527af](https://github.com/angular-ui/bootstrap/commit/12c527af), closes [#4098](https://github.com/angular-ui/bootstrap/issues/4098), [#3611](https://github.com/angular-ui/bootstrap/issues/3611))
+* **typeahead:** return `null` if empty ([c7d3a660](https://github.com/angular-ui/bootstrap/commit/c7d3a660), closes [#4078](https://github.com/angular-ui/bootstrap/issues/4078), [#3176](https://github.com/angular-ui/bootstrap/issues/3176))
+
+
+## Features
+
+* **accordion:**
+ * add `controllerAs` support ([9865ee8e](https://github.com/angular-ui/bootstrap/commit/9865ee8e), closes [#4138](https://github.com/angular-ui/bootstrap/issues/4138))
+ * add `templateUrl` support ([f777c320](https://github.com/angular-ui/bootstrap/commit/f777c320), closes [#4084](https://github.com/angular-ui/bootstrap/issues/4084))
+* **alert:** add `templateUrl` support ([88a885ca](https://github.com/angular-ui/bootstrap/commit/88a885ca), closes [#4139](https://github.com/angular-ui/bootstrap/issues/4139))
+* **buttons:** add `controllerAs` support ([02872dc1](https://github.com/angular-ui/bootstrap/commit/02872dc1), closes [#4140](https://github.com/angular-ui/bootstrap/issues/4140))
+* **carousel:**
+ * add `templateUrl` support ([a29c8f20](https://github.com/angular-ui/bootstrap/commit/a29c8f20), closes [#4141](https://github.com/angular-ui/bootstrap/issues/4141))
+ * expose carousel controller via `controllerAs` ([bfec07e4](https://github.com/angular-ui/bootstrap/commit/bfec07e4), closes [#4131](https://github.com/angular-ui/bootstrap/issues/4131))
+* **datepicker:**
+ * allow custom templates ([e04b06d7](https://github.com/angular-ui/bootstrap/commit/e04b06d7), closes [#4157](https://github.com/angular-ui/bootstrap/issues/4157), [#1913](https://github.com/angular-ui/bootstrap/issues/1913))
+ * add `onOpenFocus` support ([68afc4c6](https://github.com/angular-ui/bootstrap/commit/68afc4c6), closes [#2303](https://github.com/angular-ui/bootstrap/issues/2303), [#2546](https://github.com/angular-ui/bootstrap/issues/2546), [#4146](https://github.com/angular-ui/bootstrap/issues/4146))
+ * add support for dynamic `min-mode` and `max-mode` ([f3d263e1](https://github.com/angular-ui/bootstrap/commit/f3d263e1), closes [#3843](https://github.com/angular-ui/bootstrap/issues/3843), [#2618](https://github.com/angular-ui/bootstrap/issues/2618))
+ * allow suppression of log error ([bab1d375](https://github.com/angular-ui/bootstrap/commit/bab1d375), closes [#3836](https://github.com/angular-ui/bootstrap/issues/3836), [#4115](https://github.com/angular-ui/bootstrap/issues/4115))
+* **docs:**
+ * add explanation of eye icon ([265d429b](https://github.com/angular-ui/bootstrap/commit/265d429b), closes [#4120](https://github.com/angular-ui/bootstrap/issues/4120))
+ * add ngAnimate Plunker support ([a8a22cff](https://github.com/angular-ui/bootstrap/commit/a8a22cff), closes [#3648](https://github.com/angular-ui/bootstrap/issues/3648), [#4072](https://github.com/angular-ui/bootstrap/issues/4072))
+* **modal:**
+ * add ability to change class on body ([5a28ff76](https://github.com/angular-ui/bootstrap/commit/5a28ff76), closes [#2633](https://github.com/angular-ui/bootstrap/issues/2633), [#4132](https://github.com/angular-ui/bootstrap/issues/4132))
+ * allow users to resolve with strings ([89368856](https://github.com/angular-ui/bootstrap/commit/89368856), closes [#2676](https://github.com/angular-ui/bootstrap/issues/2676), [#4124](https://github.com/angular-ui/bootstrap/issues/4124))
+* **pagination:**
+ * add classes to assist with styling ([b21c9abd](https://github.com/angular-ui/bootstrap/commit/b21c9abd), closes [#4130](https://github.com/angular-ui/bootstrap/issues/4130), [#4142](https://github.com/angular-ui/bootstrap/issues/4142))
+ * add `templateUrl` support ([a0e1c91c](https://github.com/angular-ui/bootstrap/commit/a0e1c91c), closes [#4137](https://github.com/angular-ui/bootstrap/issues/4137))
+* **timepicker:**
+ * add documentation for max/min ([87fc242d](https://github.com/angular-ui/bootstrap/commit/87fc242d))
+ * Added min/max attributes for timepicker. ([6c0010be](https://github.com/angular-ui/bootstrap/commit/6c0010be), closes [#4019](https://github.com/angular-ui/bootstrap/issues/4019))
+* **tooltip:**
+ * remove unnecessary `$digest` ([901a7c66](https://github.com/angular-ui/bootstrap/commit/901a7c66), closes [#4151](https://github.com/angular-ui/bootstrap/issues/4151))
+ * add multiple trigger support ([ca9196fa](https://github.com/angular-ui/bootstrap/commit/ca9196fa), closes [#3987](https://github.com/angular-ui/bootstrap/issues/3987), [#4077](https://github.com/angular-ui/bootstrap/issues/4077))
+
+
+## Breaking Changes
+
+* add `open` class to accordion group when expanded
+
+Closes #4152
+Closes #3419
+
+ ([ead15e37](https://github.com/angular-ui/bootstrap/commit/ead15e37))
+* Allow the user to hit `esc` inside an element in a modal and not exit the modal if the event has been `defaultPrevented`
+
+Closes #3551
+Fixes #2544
+
+ ([a05b9c1a](https://github.com/angular-ui/bootstrap/commit/a05b9c1a))
+* Change validation key to `dateDisabled` to align better with Angular's convention
+
+Closes #2773
+Closes #4080
+
+ ([5245ccad](https://github.com/angular-ui/bootstrap/commit/5245ccad))
+
+
+
+# [0.13.2](https://github.com/angular-ui/bootstrap/compare/0.13.1...0.13.2) (2015-08-02)
+
+
+## Bug Fixes
+
+* **accordion:** apply disabled style to accordion-header ([0643fd3e](https://github.com/angular-ui/bootstrap/commit/0643fd3e), closes [#3599](https://github.com/angular-ui/bootstrap/issues/3599), [#3233](https://github.com/angular-ui/bootstrap/issues/3233))
+* **buttons:** respect disabled attribute ([42e1af5c](https://github.com/angular-ui/bootstrap/commit/42e1af5c), closes [#4026](https://github.com/angular-ui/bootstrap/issues/4026), [#4013](https://github.com/angular-ui/bootstrap/issues/4013))
+* **carousel:**
+ * fix animations with 1.4 ([f45b4a4c](https://github.com/angular-ui/bootstrap/commit/f45b4a4c), closes [#3946](https://github.com/angular-ui/bootstrap/issues/3946), [#4041](https://github.com/angular-ui/bootstrap/issues/4041), [#3811](https://github.com/angular-ui/bootstrap/issues/3811))
+ * clear `currentSlide` when there are no slides ([0c78026b](https://github.com/angular-ui/bootstrap/commit/0c78026b), closes [#4021](https://github.com/angular-ui/bootstrap/issues/4021))
+* **dateparser:** add type and validity check ([4f1e03f1](https://github.com/angular-ui/bootstrap/commit/4f1e03f1), closes [#3701](https://github.com/angular-ui/bootstrap/issues/3701), [#3759](https://github.com/angular-ui/bootstrap/issues/3759), [#3933](https://github.com/angular-ui/bootstrap/issues/3933), [#3609](https://github.com/angular-ui/bootstrap/issues/3609), [#3713](https://github.com/angular-ui/bootstrap/issues/3713), [#3736](https://github.com/angular-ui/bootstrap/issues/3736), [#3875](https://github.com/angular-ui/bootstrap/issues/3875), [#3937](https://github.com/angular-ui/bootstrap/issues/3937), [#3976](https://github.com/angular-ui/bootstrap/issues/3976))
+* **datepicker:**
+ * change to contains ([9f73d240](https://github.com/angular-ui/bootstrap/commit/9f73d240), closes [#4066](https://github.com/angular-ui/bootstrap/issues/4066), [#3076](https://github.com/angular-ui/bootstrap/issues/3076))
+ * *BREAKING CHANGE* remove `new Date` fallback ([ab4580fd](https://github.com/angular-ui/bootstrap/commit/ab4580fd), closes [#2513](https://github.com/angular-ui/bootstrap/issues/2513), [#3294](https://github.com/angular-ui/bootstrap/issues/3294), [#3344](https://github.com/angular-ui/bootstrap/issues/3344), [#3682](https://github.com/angular-ui/bootstrap/issues/3682), [#4092](https://github.com/angular-ui/bootstrap/issues/4092), [#1289](https://github.com/angular-ui/bootstrap/issues/1289), [#2446](https://github.com/angular-ui/bootstrap/issues/2446), [#3037](https://github.com/angular-ui/bootstrap/issues/3037), [#3104](https://github.com/angular-ui/bootstrap/issues/3104), [#3196](https://github.com/angular-ui/bootstrap/issues/3196), [#3206](https://github.com/angular-ui/bootstrap/issues/3206), [#3342](https://github.com/angular-ui/bootstrap/issues/3342), [#3617](https://github.com/angular-ui/bootstrap/issues/3617), [#3644](https://github.com/angular-ui/bootstrap/issues/3644))
+ * ensure `initDate` is on an object ([577b2a2a](https://github.com/angular-ui/bootstrap/commit/577b2a2a), closes [#3625](https://github.com/angular-ui/bootstrap/issues/3625))
+ * change to higher max date ([32e73280](https://github.com/angular-ui/bootstrap/commit/32e73280), closes [#4042](https://github.com/angular-ui/bootstrap/issues/4042))
+ * fix validation with `ngRequired` ([fe0d954a](https://github.com/angular-ui/bootstrap/commit/fe0d954a), closes [#4002](https://github.com/angular-ui/bootstrap/issues/4002), [#3862](https://github.com/angular-ui/bootstrap/issues/3862))
+ * set to `null` if not present ([a65a5fa1](https://github.com/angular-ui/bootstrap/commit/a65a5fa1), closes [#4014](https://github.com/angular-ui/bootstrap/issues/4014))
+* **dropdown:** add safety check for setIsOpen ([60e43160](https://github.com/angular-ui/bootstrap/commit/60e43160), closes [#4030](https://github.com/angular-ui/bootstrap/issues/4030))
+* **modal:**
+ * properly garbage collect DOM node ([1e8297be](https://github.com/angular-ui/bootstrap/commit/1e8297be), closes [#2875](https://github.com/angular-ui/bootstrap/issues/2875))
+ * fix `bindToController` implementation ([811bf96e](https://github.com/angular-ui/bootstrap/commit/811bf96e), closes [#4054](https://github.com/angular-ui/bootstrap/issues/4054), [#4051](https://github.com/angular-ui/bootstrap/issues/4051))
+ * animate backdrop concurrently with window ([c55ee4f5](https://github.com/angular-ui/bootstrap/commit/c55ee4f5), closes [#4039](https://github.com/angular-ui/bootstrap/issues/4039), [#4036](https://github.com/angular-ui/bootstrap/issues/4036))
+* **progressbar:**
+ * use more visible color ([1afc5d1d](https://github.com/angular-ui/bootstrap/commit/1afc5d1d), closes [#4044](https://github.com/angular-ui/bootstrap/issues/4044))
+ * allow max width of 100% ([2e9177e5](https://github.com/angular-ui/bootstrap/commit/2e9177e5), closes [#4027](https://github.com/angular-ui/bootstrap/issues/4027), [#4018](https://github.com/angular-ui/bootstrap/issues/4018))
+* **tooltip:**
+ * update tooltip placement dynamically ([13df1c93](https://github.com/angular-ui/bootstrap/commit/13df1c93), closes [#3980](https://github.com/angular-ui/bootstrap/issues/3980), [#3978](https://github.com/angular-ui/bootstrap/issues/3978))
+ * prevent 1px shift in Webkit/Blink ([632aa820](https://github.com/angular-ui/bootstrap/commit/632aa820), closes [#3964](https://github.com/angular-ui/bootstrap/issues/3964))
+* **typeahead:**
+ * reset matches if enter is hit ([25704838](https://github.com/angular-ui/bootstrap/commit/25704838), closes [#4063](https://github.com/angular-ui/bootstrap/issues/4063), [#3545](https://github.com/angular-ui/bootstrap/issues/3545))
+ * only reset matches if matches are present ([97e077e1](https://github.com/angular-ui/bootstrap/commit/97e077e1), closes [#3119](https://github.com/angular-ui/bootstrap/issues/3119))
+
+
+## Features
+
+* **build:** add support for npm publishing ([27f7ca26](https://github.com/angular-ui/bootstrap/commit/27f7ca26), closes [#3108](https://github.com/angular-ui/bootstrap/issues/3108))
+* **modal:** trap focus in modal for tabbing ([a028d2aa](https://github.com/angular-ui/bootstrap/commit/a028d2aa), closes [#3689](https://github.com/angular-ui/bootstrap/issues/3689), [#4004](https://github.com/angular-ui/bootstrap/issues/4004), [#738](https://github.com/angular-ui/bootstrap/issues/738))
+* **popover:** add custom template support ([a9d3d253](https://github.com/angular-ui/bootstrap/commit/a9d3d253), closes [#4056](https://github.com/angular-ui/bootstrap/issues/4056), [#4057](https://github.com/angular-ui/bootstrap/issues/4057))
+* **rating:** add title support for stars ([713c8487](https://github.com/angular-ui/bootstrap/commit/713c8487), closes [#3621](https://github.com/angular-ui/bootstrap/issues/3621))
+* **typeahead:**
+ * add `noResults` indicator binding ([647cdd93](https://github.com/angular-ui/bootstrap/commit/647cdd93), closes [#2016](https://github.com/angular-ui/bootstrap/issues/2016), [#2792](https://github.com/angular-ui/bootstrap/issues/2792), [#4068](https://github.com/angular-ui/bootstrap/issues/4068))
+ * add `typeaheadSelectOnExact` support ([277b30ca](https://github.com/angular-ui/bootstrap/commit/277b30ca), closes [#3365](https://github.com/angular-ui/bootstrap/issues/3365), [#3310](https://github.com/angular-ui/bootstrap/issues/3310))
+
+
+
+# [0.13.1](https://github.com/angular-ui/bootstrap/compare/0.13.0...0.13.1) (2015-07-23)
+
+
+## Bug Fixes
+
+* **accordion:** add CSP compliance for accordion & typeahead ([fb302c60](https://github.com/angular-ui/bootstrap/commit/fb302c60), closes [#3909](https://github.com/angular-ui/bootstrap/issues/3909), [#3904](https://github.com/angular-ui/bootstrap/issues/3904))
+* **alert:**
+ * adjust check for close attribute ([13a0354f](https://github.com/angular-ui/bootstrap/commit/13a0354f), closes [#3864](https://github.com/angular-ui/bootstrap/issues/3864), [#3890](https://github.com/angular-ui/bootstrap/issues/3890), [#3848](https://github.com/angular-ui/bootstrap/issues/3848))
+ * rename alert-dismissable to alert-dismissible ([d631af5a](https://github.com/angular-ui/bootstrap/commit/d631af5a))
+* **carousel:**
+ * change to avoid references to debug info ([ca07ad7c](https://github.com/angular-ui/bootstrap/commit/ca07ad7c), closes [#3795](https://github.com/angular-ui/bootstrap/issues/3795), [#3794](https://github.com/angular-ui/bootstrap/issues/3794))
+ * ensure there are slides present ([115d490a](https://github.com/angular-ui/bootstrap/commit/115d490a), closes [#3755](https://github.com/angular-ui/bootstrap/issues/3755))
+ * disable transition until animation completes ([ef45ecf8](https://github.com/angular-ui/bootstrap/commit/ef45ecf8), closes [#3729](https://github.com/angular-ui/bootstrap/issues/3729), [#3757](https://github.com/angular-ui/bootstrap/issues/3757))
+* **collapse:** fix occasional flickering ([ede9ea46](https://github.com/angular-ui/bootstrap/commit/ede9ea46), closes [#3804](https://github.com/angular-ui/bootstrap/issues/3804), [#3801](https://github.com/angular-ui/bootstrap/issues/3801))
+* **datepicker:**
+ * change to min width cells ([5567c432](https://github.com/angular-ui/bootstrap/commit/5567c432), closes [#4000](https://github.com/angular-ui/bootstrap/issues/4000), [#3941](https://github.com/angular-ui/bootstrap/issues/3941))
+ * fix OS dependent time zone issue ([f1412014](https://github.com/angular-ui/bootstrap/commit/f1412014), closes [#3079](https://github.com/angular-ui/bootstrap/issues/3079))
+ * Apply custom class to month ([eb3b32ec](https://github.com/angular-ui/bootstrap/commit/eb3b32ec), closes [#3863](https://github.com/angular-ui/bootstrap/issues/3863))
+ * check if getter.assign is function ([ed10899d](https://github.com/angular-ui/bootstrap/commit/ed10899d), closes [#3155](https://github.com/angular-ui/bootstrap/issues/3155), [#3345](https://github.com/angular-ui/bootstrap/issues/3345), [#3719](https://github.com/angular-ui/bootstrap/issues/3719))
+* **dropdown:**
+ * do not autoclose with outsideClick and append to body ([cc66a068](https://github.com/angular-ui/bootstrap/commit/cc66a068), closes [#3792](https://github.com/angular-ui/bootstrap/issues/3792), [#3645](https://github.com/angular-ui/bootstrap/issues/3645))
+ * avoid matching 138 & 140 ([41ebd984](https://github.com/angular-ui/bootstrap/commit/41ebd984))
+ * align when using dropdown-menu-body ([2332f14d](https://github.com/angular-ui/bootstrap/commit/2332f14d), closes [#3913](https://github.com/angular-ui/bootstrap/issues/3913), [#3820](https://github.com/angular-ui/bootstrap/issues/3820))
+ * call toggle after animation ([054341b7](https://github.com/angular-ui/bootstrap/commit/054341b7), closes [#3513](https://github.com/angular-ui/bootstrap/issues/3513), [#3655](https://github.com/angular-ui/bootstrap/issues/3655), [#3511](https://github.com/angular-ui/bootstrap/issues/3511))
+ * do not close on $locationChangeSuccess ([e5a1e88f](https://github.com/angular-ui/bootstrap/commit/e5a1e88f), closes [#3683](https://github.com/angular-ui/bootstrap/issues/3683), [#3704](https://github.com/angular-ui/bootstrap/issues/3704))
+* **modal:**
+ * backdrop animation on AngularJS 1.4 ([158d2676](https://github.com/angular-ui/bootstrap/commit/158d2676), closes [#3896](https://github.com/angular-ui/bootstrap/issues/3896))
+ * closing breaks on missing scope, 1.4 ([0286828b](https://github.com/angular-ui/bootstrap/commit/0286828b), closes [#3787](https://github.com/angular-ui/bootstrap/issues/3787), [#3806](https://github.com/angular-ui/bootstrap/issues/3806), [#3873](https://github.com/angular-ui/bootstrap/issues/3873), [#3888](https://github.com/angular-ui/bootstrap/issues/3888))
+ * remove illegal character ([dd4f3cc8](https://github.com/angular-ui/bootstrap/commit/dd4f3cc8), closes [#3893](https://github.com/angular-ui/bootstrap/issues/3893), [#3892](https://github.com/angular-ui/bootstrap/issues/3892))
+ * focus on body if element disappears ([988336cc](https://github.com/angular-ui/bootstrap/commit/988336cc), closes [#3653](https://github.com/angular-ui/bootstrap/issues/3653), [#3639](https://github.com/angular-ui/bootstrap/issues/3639))
+* **progressbar:** use max value on stacked progress bar ([36e0f0ea](https://github.com/angular-ui/bootstrap/commit/36e0f0ea), closes [#3830](https://github.com/angular-ui/bootstrap/issues/3830), [#3618](https://github.com/angular-ui/bootstrap/issues/3618))
+* **rating:** Set rating to 0 when same value is selected ([dbceec76](https://github.com/angular-ui/bootstrap/commit/dbceec76), closes [#3963](https://github.com/angular-ui/bootstrap/issues/3963), [#3246](https://github.com/angular-ui/bootstrap/issues/3246))
+* **tabs:** fix empty href ([2b27dcbf](https://github.com/angular-ui/bootstrap/commit/2b27dcbf), closes [#3799](https://github.com/angular-ui/bootstrap/issues/3799))
+* **typeahead:**
+ * select match on tab for iOS webview ([5b37bb8b](https://github.com/angular-ui/bootstrap/commit/5b37bb8b), closes [#3762](https://github.com/angular-ui/bootstrap/issues/3762), [#3699](https://github.com/angular-ui/bootstrap/issues/3699))
+ * don't close popup on right click ([7d1c4600](https://github.com/angular-ui/bootstrap/commit/7d1c4600), closes [#3975](https://github.com/angular-ui/bootstrap/issues/3975), [#3973](https://github.com/angular-ui/bootstrap/issues/3973))
+ * close dropdown on tab with no selection ([493510d0](https://github.com/angular-ui/bootstrap/commit/493510d0), closes [#3340](https://github.com/angular-ui/bootstrap/issues/3340))
+ * do not execute unnecessary $digest ([0d96221f](https://github.com/angular-ui/bootstrap/commit/0d96221f), closes [#2652](https://github.com/angular-ui/bootstrap/issues/2652), [#3791](https://github.com/angular-ui/bootstrap/issues/3791))
+ * add href to show cursor as pointer ([195e520e](https://github.com/angular-ui/bootstrap/commit/195e520e), closes [#3649](https://github.com/angular-ui/bootstrap/issues/3649))
+
+
+## Features
+
+* **alert:** pass $event to close() ([44e06425](https://github.com/angular-ui/bootstrap/commit/44e06425), closes [#3828](https://github.com/angular-ui/bootstrap/issues/3828), [#3827](https://github.com/angular-ui/bootstrap/issues/3827))
+* **carousel:** add `noWrap` option to prevent re-cycling of slides ([7fb3840f](https://github.com/angular-ui/bootstrap/commit/7fb3840f), closes [#3462](https://github.com/angular-ui/bootstrap/issues/3462), [#3397](https://github.com/angular-ui/bootstrap/issues/3397))
+* **collapse:** add accessibility support ([92551342](https://github.com/angular-ui/bootstrap/commit/92551342), closes [#3920](https://github.com/angular-ui/bootstrap/issues/3920))
+* **dropdown:**
+ * add dropdown classes dynamically ([4af83ade](https://github.com/angular-ui/bootstrap/commit/4af83ade), closes [#3984](https://github.com/angular-ui/bootstrap/issues/3984), [#3986](https://github.com/angular-ui/bootstrap/issues/3986))
+ * add accessibility attributes ([14689e05](https://github.com/angular-ui/bootstrap/commit/14689e05), closes [#3951](https://github.com/angular-ui/bootstrap/issues/3951))
+ * add keynav support to dropdown ([62359370](https://github.com/angular-ui/bootstrap/commit/62359370), closes [#3685](https://github.com/angular-ui/bootstrap/issues/3685), [#3212](https://github.com/angular-ui/bootstrap/issues/3212), [#1228](https://github.com/angular-ui/bootstrap/issues/1228))
+ * support optional templates for dropdown menus ([83c4266c](https://github.com/angular-ui/bootstrap/commit/83c4266c))
+* **modal:** add support for bindToController ([8adfc833](https://github.com/angular-ui/bootstrap/commit/8adfc833), closes [#3965](https://github.com/angular-ui/bootstrap/issues/3965), [#3404](https://github.com/angular-ui/bootstrap/issues/3404))
+* **pagination:** add support for `ng-disabled` ([f6edfa5d](https://github.com/angular-ui/bootstrap/commit/f6edfa5d), closes [#3956](https://github.com/angular-ui/bootstrap/issues/3956))
+* **timepicker:** add `showSpinner` flag ([1f760eb3](https://github.com/angular-ui/bootstrap/commit/1f760eb3))
+* **typeahead:**
+ * add 'select on blur' option. ([68cac59a](https://github.com/angular-ui/bootstrap/commit/68cac59a), closes [#3445](https://github.com/angular-ui/bootstrap/issues/3445))
+ * popup position ([86bfec19](https://github.com/angular-ui/bootstrap/commit/86bfec19), closes [#3874](https://github.com/angular-ui/bootstrap/issues/3874))
+ * handles min-length of 0 ([a5a25141](https://github.com/angular-ui/bootstrap/commit/a5a25141), closes [#3600](https://github.com/angular-ui/bootstrap/issues/3600))
+
+
-## 0.13.0 (2015-05-02)
+# [0.13.0](https://github.com/angular-ui/bootstrap/compare/0.12.1...0.13.0) (2015-05-02)
-#### Bug Fixes
+## Bug Fixes
* **accordion:**
* Made accordion heading tab-able for IE9-10 ([6abad509](https://github.com/angular-ui/bootstrap/commit/6abad509cd4d44c3ca432f2f21c9ecea0a206b53))
@@ -63,7 +1213,7 @@
* $compile match template after adding to DOM ([03446c56](https://github.com/angular-ui/bootstrap/commit/03446c56335d5ee0970f9730af215fea1dd53f48))
-#### Features
+## Features
* **dateparser:**
* Add support for HH, H, mm, m, ss, s formats ([971a1b57](https://github.com/angular-ui/bootstrap/commit/971a1b57394c1e1088564e0809c1341620586aa0), closes [#2509](https://github.com/angular-ui/bootstrap/issues/2509), [#3159](https://github.com/angular-ui/bootstrap/issues/3159), [#3417](https://github.com/angular-ui/bootstrap/issues/3417))
@@ -98,15 +1248,16 @@
* **transition:** deprecate transition module ([8a552443](https://github.com/angular-ui/bootstrap/commit/8a552443741d1e5b4b29d9da9c7e9990fa37886c), closes [#3497](https://github.com/angular-ui/bootstrap/issues/3497))
-# 0.12.1 (2015-02-20)
+
+# [0.12.1](https://github.com/angular-ui/bootstrap/compare/0.12.0...0.12.1) (2015-02-20)
## Bug Fixes
-- **tooltip:**
- - incorrect position when text wraps ([5726e3ef](http://github.com/angular-ui/bootstrap/commit/5726e3ef))
+- **tooltip:**
+ - incorrect position when text wraps ([5726e3ef](http://github.com/angular-ui/bootstrap/commit/5726e3ef))
-# 0.12.0 (2014-11-16)
+# [0.12.0](https://github.com/angular-ui/bootstrap/compare/0.11.2...0.12.0) (2014-11-16)
## Bug Fixes
@@ -143,7 +1294,7 @@
* `tooltip-trigger` and `popover-trigger` are no longer watched
attributes.
-([a65bea95](http://github.com/angular-ui/bootstrap/commit/a65bea95338802b026fd213805b095b5a0b5b393))
+([a65bea95](http://github.com/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.
@@ -159,133 +1310,136 @@ once* and can no longer be changed after initialization.
```
-# 0.11.2 (2014-09-26)
+
+# [0.11.2](https://github.com/angular-ui/bootstrap/compare/0.11.1...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)
+
+# [0.11.1](https://github.com/angular-ui/bootstrap/compare/0.11.0...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))
+- **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)
+- **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](https://github.com/angular-ui/bootstrap/compare/0.10.0...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))
+- **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))
+- **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))
+ - 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))
+ - 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))
+- **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:**
+- **alert:**
Use interpolation for type attribute.
Before:
@@ -308,13 +1462,14 @@ Revert breaking change in **dropdown** ([1a998c4](http://github.com/angular-ui/b
```
-- **datepicker:**
+- **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`
-`Open on focus` has been removed. Read more on this ([comment](https://github.com/angular-ui/bootstrap/pull/1922#issuecomment-40491716)).
+`Open on focus` has been removed. Read more on this ([comment](https://github.com/angular-ui/bootstrap/pull/1922#issuecomment-40491716)).
+`dateFormat` renamed to `datepickerPopup` in datepickerPopupConfig
- **dropdown:**
@@ -327,18 +1482,18 @@ Revert breaking change in **dropdown** ([1a998c4](http://github.com/angular-ui/b
* `on-select-page` is removed since `ng-change` can now be used.
Before:
-
+
```html
```
-
+
After:
```html
```
-
-- **rating:**
+
+- **rating:**
`rating` is now integrated with `ngModelController`.
* `value` is replaced from `ng-model`.
@@ -347,142 +1502,145 @@ Revert breaking change in **dropdown** ([1a998c4](http://github.com/angular-ui/b
```html
```
-
+
After:
-
+
```html
```
-
+
- **tabs:**
Use interpolation for type attribute.
Before:
-
+
```html
```
-
+
After:
-
+
```html
```
-
-# 0.10.0 (2014-01-13)
+
+
+# [0.10.0](https://github.com/angular-ui/bootstrap/compare/0.9.0...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))
+- **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))
+- **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)
+
+# [0.9.0](https://github.com/angular-ui/bootstrap/compare/0.8.0...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))
+- **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))
+- **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)
+
+# [0.8.0](https://github.com/angular-ui/bootstrap/compare/0.7.0...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))
+- **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))
+- **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))
+ - 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:**
+- **progressbar:**
The onFull/onEmpty handlers & auto/stacked types have been removed.
To migrate your code change your markup like below.
@@ -505,76 +1663,78 @@ _This release adds Bootstrap3 support_
```
-# 0.7.0 (2013-11-22)
+
+# [0.7.0](https://github.com/angular-ui/bootstrap/compare/0.6.0...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))
+- **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))
+- **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))
+ - 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)
+ - 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](https://github.com/angular-ui/bootstrap/compare/0.6.0...0.7.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))
+- **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))
+- **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))
@@ -583,21 +1743,21 @@ _This release adds Bootstrap3 support_
- 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))
+ - 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
@@ -608,7 +1768,7 @@ _This release adds Bootstrap3 support_
Check the documentation for the `$modal` service to migrate from `$dialog`
-- **pagination:**
+- **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` &
@@ -627,65 +1787,66 @@ Check the documentation for the `$modal` service to migrate from `$dialog`
```
-- **tooltip:**
+- **tooltip:**
The placment='mouse' is gone with no equivalent
-
-# 0.5.0 (2013-08-04)
+
+
+# [0.5.0](https://github.com/angular-ui/bootstrap/compare/0.4.0...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))
+- **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))
+- **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:**
+- **pagination:**
The 'first-text', 'previous-text', 'next-text' and 'last-text'
attributes are now interpolated.
@@ -715,93 +1876,94 @@ The placment='mouse' is gone with no equivalent
```
-# 0.4.0 (2013-06-24)
+
+# [0.4.0](https://github.com/angular-ui/bootstrap/compare/0.3.0...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:**
+- **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))
+ - 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:**
+- **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))
+- **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:**
+- **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
```
After:
-
+
```html
```
-- **progressbar:**
+- **progressbar:**
The 'value' is replaced by 'percent'.
Before:
-
+
```html
```
After:
-
+
```html
```
-- **tabs:**
+- **tabs:**
The 'tabs' directive has been renamed to 'tabset', and
the 'pane' directive has been renamed to 'tab'.
@@ -833,8 +1995,8 @@ The placment='mouse' is gone with no equivalent
```
-
-# 0.3.0 (2013-04-30)
+
+# [0.3.0](https://github.com/angular-ui/bootstrap/compare/0.2.0...0.3.0) (2013-04-30)
## Features
@@ -875,7 +2037,8 @@ The placment='mouse' is gone with no equivalent
- 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)
+
+# [0.2.0](https://github.com/angular-ui/bootstrap/compare/0.1.0...0.2.0) (2013-03-03)
## Features
@@ -909,6 +2072,7 @@ The placment='mouse' is gone with no equivalent
- **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_.
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000000..65c05c5748
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,22 @@
+# Contributor Code of Conduct
+
+As contributors and maintainers of this project, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.
+
+We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, or nationality.
+
+Examples of unacceptable behavior by participants include:
+
+* The use of sexualized language or imagery
+* Personal attacks
+* Trolling or insulting/derogatory comments
+* Public or private harassment
+* Publishing other's private information, such as physical or electronic addresses, without explicit permission
+* Other unethical or unprofessional conduct.
+
+Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. By adopting this Code of Conduct, project maintainers commit themselves to fairly and consistently applying these principles to every aspect of managing this project. Project maintainers who do not follow or enforce the Code of Conduct may be permanently removed from the project team.
+
+This code of conduct applies both within project spaces and in public spaces when an individual is representing the project or its community.
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers.
+
+This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.2.0, available at [http://contributor-covenant.org/version/1/2/0/](http://contributor-covenant.org/version/1/2/0/)
\ No newline at end of file
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 14514ba896..40be9ce425 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -2,14 +2,14 @@
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`.
+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 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.
+To save your and our time we will be systematically closing all the issues that are requests for general support and redirecting people to StackOverflow.
## You think you've found a bug?
@@ -17,9 +17,9 @@ Oh, we are ashamed and want to fix it asap! But before fixing a bug we need to r
* 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
+* 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.
+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.
@@ -42,3 +42,4 @@ We are always looking for the quality contributions and will be happy to accept
* 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
+ * your commits conform to the conventions established [here](https://github.com/stevemao/conventional-changelog-angular/blob/master/convention.md)
diff --git a/Gruntfile.js b/Gruntfile.js
index 3555901de1..ed0ec0576a 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -1,26 +1,16 @@
-/* jshint node: true */
-var markdown = require('node-markdown').Markdown;
+var marked = require('marked');
var fs = require('fs');
+var _ = require('lodash');
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');
+ require('load-grunt-tasks')(grunt);
// Project configuration.
grunt.util.linefeed = '\n';
grunt.initConfig({
- ngversion: '1.3.13',
- bsversion: '3.1.1',
+ ngversion: '1.6.1',
+ bsversion: '3.3.7',
modules: [],//to be filled in by build task
pkg: grunt.file.readJSON('package.json'),
dist: 'dist',
@@ -33,12 +23,14 @@ module.exports = function(grunt) {
cssInclude: '',
cssFileBanner: '/* Include this file in your html if you are using the CSP mode. */\n\n',
cssFileDest: '<%= dist %>/<%= filename %>-<%= pkg.version %>-csp.css',
- banner: ['/*',
- ' * <%= pkg.name %>',
- ' * <%= pkg.homepage %>\n',
- ' * Version: <%= pkg.version %> - <%= grunt.template.today("yyyy-mm-dd") %>',
- ' * License: <%= pkg.license %>',
- ' */\n'].join('\n')
+ banner: [
+ '/*',
+ ' * <%= pkg.name %>',
+ ' * <%= pkg.homepage %>\n',
+ ' * Version: <%= pkg.version %> - <%= grunt.template.today("yyyy-mm-dd") %>',
+ ' * License: <%= pkg.license %>',
+ ' */'
+ ].join('\n')
},
delta: {
docs: {
@@ -50,8 +42,7 @@ module.exports = function(grunt) {
tasks: ['html2js', 'karma:watch:run']
},
js: {
- files: ['src/**/*.js'],
- //we don't need to jshint here, it slows down everything else
+ files: ['src/**/*.js', '!src/**/index.js'],
tasks: ['karma:watch:run']
}
},
@@ -113,7 +104,10 @@ module.exports = function(grunt) {
dist: {
options: {
module: null, // no bundle module for all the html2js templates
- base: '.'
+ base: '.',
+ rename: function(moduleName) {
+ return `uib/${moduleName}`;
+ }
},
files: [{
expand: true,
@@ -122,11 +116,8 @@ module.exports = function(grunt) {
}]
}
},
- jshint: {
- files: ['Gruntfile.js','src/**/*.js'],
- options: {
- jshintrc: '.jshintrc'
- }
+ eslint: {
+ files: ['Gruntfile.js','src/**/*.js']
},
karma: {
options: {
@@ -140,12 +131,14 @@ module.exports = function(grunt) {
},
jenkins: {
singleRun: true,
+ autoWatch: false,
colors: false,
reporters: ['dots', 'junit'],
browsers: ['Chrome', 'ChromeCanary', 'Firefox', 'Opera', '/Users/jenkins/bin/safari.sh']
},
travis: {
singleRun: true,
+ autoWatch: false,
reporters: ['dots'],
browsers: ['Firefox']
},
@@ -156,19 +149,24 @@ module.exports = function(grunt) {
reporters: ['progress', 'coverage']
}
},
- changelog: {
+ conventionalChangelog: {
options: {
- dest: 'CHANGELOG.md',
+ changelogOpts: {
+ preset: 'angular'
+ },
templateFile: 'misc/changelog.tpl.md'
+ },
+ release: {
+ src: 'CHANGELOG.md'
}
},
shell: {
- //We use %version% and evluate it at run-time, because <%= pkg.version %>
+ //We use %version% and evaluate it at run-time, because <%= pkg.version %>
//is only evaluated once
'release-prepare': [
'grunt before-test after-test',
'grunt version', //remove "-SNAPSHOT"
- 'grunt changelog'
+ 'grunt conventionalChangelog'
],
'release-complete': [
'git commit CHANGELOG.md package.json -m "chore(release): v%version%"',
@@ -179,25 +177,6 @@ module.exports = function(grunt) {
'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'
@@ -207,7 +186,7 @@ module.exports = function(grunt) {
//register before and after test tasks so we've don't have to change cli
//options on the google's CI server
- grunt.registerTask('before-test', ['enforce', 'ddescribe-iit', 'jshint', 'html2js']);
+ grunt.registerTask('before-test', ['enforce', 'ddescribe-iit', 'eslint', 'html2js']);
grunt.registerTask('after-test', ['build', 'copy']);
//Rename our watch task to 'delta', then make actual 'watch'
@@ -218,7 +197,7 @@ module.exports = function(grunt) {
// Default task.
grunt.registerTask('default', ['before-test', 'test', 'after-test']);
- grunt.registerTask('enforce', 'Install commit message enforce script if it doesn\'t exist', function() {
+ 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');
@@ -243,25 +222,28 @@ module.exports = function(grunt) {
});
}
function enquote(str) {
- return '"' + str + '"';
+ return `"${str}"`;
+ }
+ function enquoteUibDir(str) {
+ return enquote(`uib/${str}`);
}
var module = {
name: name,
- moduleName: enquote('ui.bootstrap.' + name),
+ moduleName: enquote(`ui.bootstrap.${name}`),
displayName: ucwords(breakup(name, ' ')),
- srcFiles: grunt.file.expand('src/'+name+'/*.js'),
- cssFiles: grunt.file.expand('src/'+name+'/*.css'),
- tplFiles: grunt.file.expand('template/'+name+'/*.html'),
- tpljsFiles: grunt.file.expand('template/'+name+'/*.html.js'),
- tplModules: grunt.file.expand('template/'+name+'/*.html').map(enquote),
+ srcFiles: grunt.file.expand([`src/${name}/*.js`, `!src/${name}/index.js`, `!src/${name}/index-nocss.js`]),
+ cssFiles: grunt.file.expand(`src/${name}/*.css`),
+ tplFiles: grunt.file.expand(`template/${name}/*.html`),
+ tpljsFiles: grunt.file.expand(`template/${name}/*.html.js`),
+ tplModules: grunt.file.expand(`template/${name}/*.html`).map(enquoteUibDir),
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')
+ md: grunt.file.expand(`src/${name}/docs/*.md`)
+ .map(grunt.file.read).map((str) => marked(str)).join('\n'),
+ js: grunt.file.expand(`src/${name}/docs/*.js`)
.map(grunt.file.read).join('\n'),
- html: grunt.file.expand('src/'+name+'/docs/*.html')
+ html: grunt.file.expand(`src/${name}/docs/*.html`)
.map(grunt.file.read).join('\n')
}
};
@@ -270,7 +252,7 @@ module.exports = function(grunt) {
css: [],
js: []
};
- module.cssFiles.forEach(processCSS.bind(null, styles, true));
+ module.cssFiles.forEach(processCSS.bind(null, module.name, styles, true));
if (styles.css.length) {
module.css = styles.css.join('\n');
module.cssJs = styles.js.join('\n');
@@ -282,7 +264,7 @@ module.exports = function(grunt) {
function dependenciesForModule(name) {
var deps = [];
- grunt.file.expand('src/' + name + '/*.js')
+ grunt.file.expand([`src/${name}/*.js`, `!src/${name}/index.js`, `!src/${name}/index-nocss.js`])
.map(grunt.file.read)
.forEach(function(contents) {
//Strategy: find where module is declared,
@@ -320,19 +302,17 @@ module.exports = function(grunt) {
} else {
grunt.file.expand({
filter: 'isDirectory', cwd: '.'
- }, 'src/*').forEach(function(dir) {
+ }, 'src/*').forEach((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('tplModules', _.pluck(modules, 'tplModules').filter((tpls) => tpls.length > 0));
grunt.config('demoModules', modules
- .filter(function(module) {
- return module.docs.md && module.docs.js && module.docs.html;
- })
- .sort(function(a, b) {
+ .filter((module) => module.docs.md && module.docs.js && module.docs.html)
+ .sort((a, b) => {
if (a.name < b.name) { return -1; }
if (a.name > b.name) { return 1; }
return 0;
@@ -351,9 +331,7 @@ module.exports = function(grunt) {
}
var moduleFileMapping = _.clone(modules, true);
- moduleFileMapping.forEach(function (module) {
- delete module.docs;
- });
+ moduleFileMapping.forEach((module) => delete module.docs);
grunt.config('moduleFileMapping', moduleFileMapping);
@@ -366,17 +344,17 @@ module.exports = function(grunt) {
grunt.config('concat.dist_tpls.src', grunt.config('concat.dist_tpls.src')
.concat(srcFiles).concat(tpljsFiles));
- grunt.task.run(['concat', 'uglify', 'makeModuleMappingFile', 'makeRawFilesJs']);
+ grunt.task.run(['concat', 'uglify', 'makeModuleMappingFile', 'makeRawFilesJs', 'makeVersionsMappingFile']);
});
- grunt.registerTask('test', 'Run tests on singleRun karma server', function () {
+ 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')) {
+ if (grunt.option('coverage')) {
var karmaOptions = grunt.config.get('karma.options'),
coverageOpts = grunt.config.get('karma.coverage');
grunt.util._.extend(karmaOptions, coverageOpts);
@@ -386,7 +364,7 @@ module.exports = function(grunt) {
}
});
- grunt.registerTask('makeModuleMappingFile', function () {
+ grunt.registerTask('makeModuleMappingFile', function() {
var _ = grunt.util._;
var moduleMappingJs = 'dist/assets/module-mapping.json';
var moduleMappings = grunt.config('moduleFileMapping');
@@ -396,7 +374,7 @@ module.exports = function(grunt) {
grunt.log.writeln('File ' + moduleMappingJs.cyan + ' created.');
});
- grunt.registerTask('makeRawFilesJs', function () {
+ grunt.registerTask('makeRawFilesJs', function() {
var _ = grunt.util._;
var jsFilename = 'dist/assets/raw-files.json';
var genRawFilesJs = require('./misc/raw-files-generator');
@@ -405,17 +383,45 @@ module.exports = function(grunt) {
grunt.config('meta.banner'), grunt.config('meta.cssFileBanner'));
});
+ grunt.registerTask('makeVersionsMappingFile', function() {
+ var done = this.async();
+
+ var exec = require('child_process').exec;
+
+ var versionsMappingFile = 'dist/versions-mapping.json';
+
+ exec('git tag --sort -version:refname', function(error, stdout, stderr) {
+ // Let's remove the oldest 14 versions.
+ var versions = stdout.split('\n').slice(0, -14);
+ var jsContent = versions.map(function(version) {
+ version = version.replace(/^v/, '');
+ return {
+ version: version,
+ url: `/bootstrap/versioned-docs/${version}`
+ };
+ });
+ jsContent = _.sortBy(jsContent, 'version').reverse();
+ jsContent.unshift({
+ version: 'Current',
+ url: '/bootstrap'
+ });
+ grunt.file.write(versionsMappingFile, JSON.stringify(jsContent));
+ grunt.log.writeln(`File ${versionsMappingFile.cyan} created.`);
+ done();
+ });
+
+ });
+
/**
* Logic from AngularJS
* https://github.com/angular/angular.js/blob/36831eccd1da37c089f2141a2c073a6db69f3e1d/lib/grunt/utils.js#L121-L145
*/
- function processCSS(state, minify, file) {
- /* jshint quotmark: false */
+ function processCSS(moduleName, state, minify, file) {
var css = fs.readFileSync(file).toString(),
js;
state.css.push(css);
- if(minify){
+ if (minify) {
css = css
.replace(/\r?\n/g, '')
.replace(/\/\*.*?\*\//g, '')
@@ -430,7 +436,7 @@ module.exports = function(grunt) {
.replace(/\\/g, '\\\\')
.replace(/'/g, "\\'")
.replace(/\r?\n/g, '\\n');
- js = "!angular.$$csp() && angular.element(document).find('head').prepend('');";
+ js = `angular.module('ui.bootstrap.${moduleName}').run(function() {!angular.$$csp().noInlineStyle && !angular.$$uib${_.capitalize(moduleName)}Css && angular.element(document).find('head').prepend(''); angular.$$uib${_.capitalize(moduleName)}Css = true; });`;
state.js.push(js);
return state;
diff --git a/LICENSE b/LICENSE
index 18db5a5313..cf7b84b3fb 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
The MIT License
-Copyright (c) 2012-2015 the AngularUI Team, https://github.com/organizations/angular-ui/teams/291112
+Copyright (c) 2012-2017 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
diff --git a/README.md b/README.md
index 4a4d595d91..8964146889 100644
--- a/README.md
+++ b/README.md
@@ -1,18 +1,31 @@
-# UI Bootstrap - [AngularJS](http://angularjs.org/) directives specific to [Bootstrap](http://getbootstrap.com)
+# Project Status (please read)
+Due to [Angular](https://angular.io)'s continued adoption, our creation of [the Angular version of this library](https://ng-bootstrap.github.io), and the the project maintainers' moving on to other things, this project is considered feature-complete and is no longer being maintained.
+
+We thank you for all your contributions over the years and hope you've enjoyed using this library as much as we've had developing and maintaining it. It would not have been successful without them.
+
+---
+
+### UI Bootstrap - [AngularJS](http://angularjs.org/) directives specific to [Bootstrap](http://getbootstrap.com)
[](https://gitter.im/angular-ui/bootstrap?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[](http://travis-ci.org/angular-ui/bootstrap)
[](https://david-dm.org/angular-ui/bootstrap#info=devDependencies)
+[](https://cdnjs.com/libraries/angular-ui-bootstrap/)
### Quick links
- [Demo](#demo)
+- [Angular 2](#angular-2)
- [Installation](#installation)
+ - [NPM](#install-with-npm)
- [Bower](#install-with-bower)
- [NuGet](#install-with-nuget)
- [Custom](#custom-build)
- [Manual](#manual-download)
+- [Webpack / JSPM](#webpack--jspm)
- [Support](#support)
- [FAQ](#faq)
+ - [Code of Conduct](#code-of-conduct)
+ - [PREFIX MIGRATION GUIDE](#prefix-migration-guide)
- [Supported browsers](#supported-browsers)
- [Need help?](#need-help)
- [Found a bug?](#found-a-bug)
@@ -22,11 +35,35 @@
# Demo
-Do you want to see directives in action? Visit http://angular-ui.github.io/bootstrap/!
+Do you want to see directives in action? Visit https://angular-ui.github.io/bootstrap/!
+
+# Angular 2
+
+Are you interested in Angular 2? We are on our way! Check out [ng-bootstrap](https://github.com/ui-bootstrap/core).
# Installation
Installation is easy as UI Bootstrap has minimal dependencies - only the AngularJS and Twitter Bootstrap's CSS are required.
+*Notes:*
+* Since version 0.13.0, UI Bootstrap depends on [ngAnimate](https://docs.angularjs.org/api/ngAnimate) for transitions and animations, such as the accordion, carousel, etc. Include `ngAnimate` in the module dependencies for your app in order to enable animation.
+* UI Bootstrap depends on [ngTouch](https://docs.angularjs.org/api/ngTouch) for swipe actions. Include `ngTouch` in the module dependencies for your app in order to enable swiping.
+
+## Angular Requirements
+* UI Bootstrap 1.0 and higher _requires_ Angular 1.4.x or higher and it has been tested with Angular 1.4.8.
+* UI Bootstrap 0.14.3 is the _last_ version that supports Angular 1.3.x.
+* UI Bootstrap 0.12.0 is the _last_ version that supports Angular 1.2.x.
+
+## Bootstrap Requirements
+* UI Bootstrap requires Bootstrap CSS version 3.x or higher and it has been tested with Bootstrap CSS 3.3.6.
+* UI Bootstrap 0.8 is the _last_ version that supports Bootstrap CSS 2.3.x.
+
+#### Install with NPM
+
+```sh
+$ npm install angular-ui-bootstrap
+```
+
+This will install AngularJS and Bootstrap NPM packages.
#### Install with Bower
```sh
@@ -44,7 +81,7 @@ PM> Install-Package Angular.UI.Bootstrap
#### Custom build
-Head over to http://angular-ui.github.io/bootstrap/ and hit the *Custom build* button to create your own custom UI Bootstrap build, just the way you like it.
+Head over to https://angular-ui.github.io/bootstrap/ and hit the *Custom build* button to create your own custom UI Bootstrap build, just the way you like it.
#### Manual download
@@ -60,12 +97,69 @@ When you are done downloading all the dependencies and project files the only re
angular.module('myModule', ['ui.bootstrap']);
```
+# Webpack / JSPM
+
+To use this project with webpack, follow the [NPM](#install-with-npm) instructions.
+Now, if you want to use only the accordion, you can do:
+
+```js
+import accordion from 'angular-ui-bootstrap/src/accordion';
+
+angular.module('myModule', [accordion]);
+```
+
+You can import all the pieces you need in the same way:
+
+```js
+import accordion from 'angular-ui-bootstrap/src/accordion';
+import datepicker from 'angular-ui-bootstrap/src/datepicker';
+
+angular.module('myModule', [accordion, datepicker]);
+```
+
+This will load all the dependencies (if any) and also the templates (if any).
+
+Be sure to have a loader able to process `css` files like `css-loader`.
+
+If you would prefer not to load your css through your JavaScript file loader/bundler, you can choose to import the `index-nocss.js` file instead, which is available for the modules:
+* carousel
+* datepicker
+* datepickerPopup
+* dropdown
+* modal
+* popover
+* position
+* timepicker
+* tooltip
+* typeahead
+
+The other modules, such as `accordion` in the example below, do not have CSS resources to load, so you should continue to import them as normal:
+
+```js
+import accordion from 'angular-ui-bootstrap/src/accordion';
+import typeahead from 'angular-ui-bootstrap/src/typeahead/index-nocss.js';
+
+angular.module('myModule', [accordion, typeahead]);
+```
+
+# Versioning
+
+Pre-2.0.0 does not follow a particular versioning system. 2.0.0 and onwards follows [semantic versioning](http://semver.org/). All release changes can be viewed on our [changelog](CHANGELOG.md).
+
# Support
## FAQ
https://github.com/angular-ui/bootstrap/wiki/FAQ
+# Code of Conduct
+
+Take a moment to read our [Code of Conduct](CODE_OF_CONDUCT.md)
+
+## PREFIX MIGRATION GUIDE
+
+If you're updating your application to use prefixes, please check the [migration guide](https://github.com/angular-ui/bootstrap/wiki/Migration-guide-for-prefixes).
+
## Supported browsers
Directives from this repository are automatically tested with the following browsers:
@@ -77,7 +171,6 @@ Directives from this repository are automatically tested with the following brow
Modern mobile browsers should work without problems.
-
## Need help?
Need help using UI Bootstrap?
diff --git a/docs/css/style.css b/docs/css/style.css
deleted file mode 100644
index 0bbd3ad262..0000000000
--- a/docs/css/style.css
+++ /dev/null
@@ -1,21 +0,0 @@
-.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;
-}
diff --git a/docs/nav.html b/docs/nav.html
deleted file mode 100644
index 078fd91200..0000000000
--- a/docs/nav.html
+++ /dev/null
@@ -1,13 +0,0 @@
-
+ For Angular 2 support, check out
+
+ ng-bootstrap
+ , created by the UI Bootstrap team.
+
Dependencies
This repository contains a set of native AngularJS directives based on
@@ -145,8 +161,11 @@
Dependencies
JavaScript is required. The only required dependencies are:
-
AngularJS (requires AngularJS 1.3.x, tested with <%= ngversion %>).
- 0.12.0 is the last version of this library that supports AngularJS 1.2.x.
+
AngularJS (requires AngularJS 1.4.x or higher, tested with <%= ngversion %>).
+ 0.14.3 is the last version of this library that supports AngularJS 1.3.x and
+ 0.12.0 is the last version that supports AngularJS 1.2.x.
+
Angular-animate (the version should match with your angular's, tested with <%= ngversion %>) if you plan in using animations, you need to load angular-animate as well.
+
Angular-touch (the version should match with your angular's, tested with <%= ngversion %>) if you plan in using swipe actions, you need to load angular-touch as well.
Bootstrap CSS (tested with version <%= bsversion %>).
This version of the library (<%= pkg.version%>) works only with Bootstrap CSS in version 3.x.
0.8.0 is the last version of this library that supports Bootstrap CSS in version 2.3.x.
@@ -156,25 +175,51 @@
Files to download
Build files for all directives are distributed in several flavours: minified for production usage, un-minified
for development, with or without templates. All the options are described and can be
- downloaded from here.
+ downloaded from here. It should be noted that the -tpls files contain the templates bundled in JavaScript, while the regular version does not contain the bundled templates. For more information, check out the FAQ here and the README here.
Alternativelly, if you are only interested in a subset of directives, you can
create your own build.
-
Whichever method you choose the good news that the overall size of a download is very small:
- <20kB for all directives (~5kB with gzip compression!)
+
Whichever method you choose the good news that the overall size of a download is fairly small:
+ 122K minified for all directives with templates and 98K without (~31kB with gzip
+ compression, with templates, and 28K gzipped without)
Installation
As soon as you've got all the files downloaded and included in your page you just need to declare
a dependency on the ui.bootstrapmodule:
angular.module('myModule', ['ui.bootstrap']);
+
If you are using UI Bootstrap in the CSP mode, e.g. in an extension, make sure you link to the ui-bootstrap-csp.css in your HTML manually.
You can fork one of the plunkers from this page to see a working example of what is described here.
+
Migration to prefixes
+
Since version 0.14.0 we started to prefix all our components. If you are upgrading from ui-bootstrap 0.13.4 or earlier,
+ check our migration guide.
CSS
Original Bootstrap's CSS depends on empty href attributes to style cursors for several components (pagination, tabs etc.).
But in AngularJS adding empty href attributes to link tags will cause unwanted route changes. This is why we need to remove empty href attributes from directive templates and as a result styling is not applied correctly. The remedy is simple, just add the following styling to your application:
.nav, .pagination, .carousel, .panel-title a { cursor: pointer; }
FAQ
Please check our FAQ section for common problems / solutions.
+
Reading the documentation
+
+ Each of the components provided in ui-bootstrap have documentation and interactive Plunker examples.
+
+
+ For the directives, we list the different attributes with their default values. In addition to this, some settings have a badge on it:
+
+
+
- This setting has an angular $watch listener applied to it.
+
B - This setting is a boolean. It doesn't need a parameter.
+
C - This setting can be configured globally in a constant service*.
+
$ - This setting expects an angular expression instead of a literal string. If the expression support a boolean / integer, you can pass it directly.
+
readonly - This setting is readonly.
+
+
+
+ For the services (you will recognize them with the $ prefix), we list all the possible parameters you can pass to them and their default values if any.
+
+
+ * Some directives have a config service that follows the next pattern: uibDirectiveConfig. The service's settings use camel case. The services can be configured in a .config function for example.
+
The body of the accordion group grows to fit the contents
-
-
{{item}}
-
-
-
- I can have markup, too!
-
- This is just some content to illustrate fancy headings.
-
-
-
\ No newline at end of file
+
+
+
The body of the uib-accordion group grows to fit the contents
+
+
{{item}}
+
+
+ Hello
+
+
+
+ Custom template with custom header template
+
+ World
+
+
+
Please, to delete your account, click the button below
+
+
+
+
+ I can have markup, too!
+
+ This is just some content to illustrate fancy headings.
+
+
+
diff --git a/src/accordion/docs/demo.js b/src/accordion/docs/demo.js
index 390bc03031..8922f7ed29 100644
--- a/src/accordion/docs/demo.js
+++ b/src/accordion/docs/demo.js
@@ -20,6 +20,7 @@ angular.module('ui.bootstrap.demo').controller('AccordionDemoCtrl', function ($s
};
$scope.status = {
+ isCustomHeaderOpen: false,
isFirstOpen: true,
isFirstDisabled: false
};
diff --git a/src/accordion/docs/readme.md b/src/accordion/docs/readme.md
index 1a67460cbe..25f790c3fa 100644
--- a/src/accordion/docs/readme.md
+++ b/src/accordion/docs/readme.md
@@ -1,10 +1,49 @@
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 into the body of the collapsible element.
-The body of each accordion group is transcluded in to the body of the collapsible element.
+### uib-accordion settings
-### Accordion Settings ###
+* `close-others`
+ $
+ C
+ _(Default: `true`)_ -
+ Control whether expanding an item will cause the other items to close.
- * `is-open` (Defaults: false) :
- Whether accordion group is open or closed.
+* `template-url`
+ _(Default: `template/accordion/accordion.html`)_ -
+ Add the ability to override the template used on the component.
+
+### uib-accordion-group settings
+
+* `heading`
+ _(Default: `none`)_ -
+ The clickable text on the group's header. You need one to be able to click on the header for toggling.
+
+* `is-disabled`
+ $
+
+ _(Default: `false`)_ -
+ Whether the accordion group is disabled or not.
+
+* `is-open`
+ $
+
+ _(Default: `false`)_ -
+ Whether accordion group is open or closed.
+
+* `template-url`
+ _(Default: `uib/template/accordion/accordion-group.html`)_ -
+ Add the ability to override the template used on the component.
+
+### Accordion heading
+
+Instead of the `heading` attribute on the `uib-accordion-group`, you can use an `uib-accordion-heading` element inside a group that will be used as the group's header.
+
+If you're using a custom template for the `uib-accordion-group`, you'll need to have an element for the heading to be transcluded into using `uib-accordion-header` (e.g. ``).
+
+### Known issues
+
+To use clickable elements within the accordion, you have to override the accordion-group template to use div elements instead of anchor elements, and add `cursor: pointer` in your CSS. This is due to browsers interpreting anchor elements as the target of any click event, which triggers routing when certain elements such as buttons are nested inside the anchor element.
+
+If custom classes on the accordion-group element are desired, one needs to either modify the template to remove the `ng-class` usage in the accordion-group template and use ng-class on the accordion-group element (not recommended), or use an interpolated expression in the class attribute, i.e. ``.
diff --git a/src/accordion/index.js b/src/accordion/index.js
new file mode 100644
index 0000000000..491b4f518f
--- /dev/null
+++ b/src/accordion/index.js
@@ -0,0 +1,11 @@
+require('../collapse');
+require('../tabindex');
+require('../../template/accordion/accordion-group.html.js');
+require('../../template/accordion/accordion.html.js');
+require('./accordion');
+
+var MODULE_NAME = 'ui.bootstrap.module.accordion';
+
+angular.module(MODULE_NAME, ['ui.bootstrap.accordion', 'uib/template/accordion/accordion.html', 'uib/template/accordion/accordion-group.html']);
+
+module.exports = MODULE_NAME;
diff --git a/src/accordion/test/accordion.spec.js b/src/accordion/test/accordion.spec.js
index 2b9ae15c59..bb64a4b2c6 100644
--- a/src/accordion/test/accordion.spec.js
+++ b/src/accordion/test/accordion.spec.js
@@ -1,21 +1,21 @@
-describe('accordion', function () {
- var $scope;
+describe('uib-accordion', function() {
+ var $animate, $scope;
beforeEach(module('ui.bootstrap.accordion'));
- beforeEach(module('ui.bootstrap.collapse'));
- beforeEach(module('template/accordion/accordion.html'));
- beforeEach(module('template/accordion/accordion-group.html'));
+ beforeEach(module('ngAnimateMock'));
+ beforeEach(module('uib/template/accordion/accordion.html'));
+ beforeEach(module('uib/template/accordion/accordion-group.html'));
- beforeEach(inject(function ($rootScope) {
+ beforeEach(inject(function(_$animate_, $rootScope) {
+ $animate = _$animate_;
$scope = $rootScope;
}));
describe('controller', function () {
-
var ctrl, $element, $attrs;
beforeEach(inject(function($controller) {
- $attrs = {}; $element = {};
- ctrl = $controller('AccordionController', { $scope: $scope, $element: $element, $attrs: $attrs });
+ $attrs = {};
+ ctrl = $controller('UibAccordionController', { $scope: $scope, $attrs: $attrs });
}));
describe('addGroup', function() {
@@ -36,6 +36,7 @@ describe('accordion', function () {
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);
@@ -62,13 +63,14 @@ describe('accordion', function () {
describe('setting accordionConfig', function() {
var originalCloseOthers;
- beforeEach(inject(function(accordionConfig) {
- originalCloseOthers = accordionConfig.closeOthers;
- accordionConfig.closeOthers = false;
+ beforeEach(inject(function(uibAccordionConfig) {
+ originalCloseOthers = uibAccordionConfig.closeOthers;
+ uibAccordionConfig.closeOthers = false;
}));
- afterEach(inject(function(accordionConfig) {
+
+ afterEach(inject(function(uibAccordionConfig) {
// return it to the original value
- accordionConfig.closeOthers = originalCloseOthers;
+ uibAccordionConfig.closeOthers = originalCloseOthers;
}));
it('should not close other panels if accordionConfig.closeOthers is false', function() {
@@ -81,7 +83,7 @@ describe('accordion', function () {
});
describe('removeGroup', function() {
- it('should remove the specified panel', function () {
+ it('should remove the specified panel', function() {
var group1, group2, group3;
ctrl.addGroup(group1 = $scope.$new());
ctrl.addGroup(group2 = $scope.$new());
@@ -91,7 +93,7 @@ describe('accordion', function () {
expect(ctrl.groups[0]).toBe(group1);
expect(ctrl.groups[1]).toBe(group3);
});
- it('should ignore remove of non-existing panel', function () {
+ it('should ignore remove of non-existing panel', function() {
var group1, group2;
ctrl.addGroup(group1 = $scope.$new());
ctrl.addGroup(group2 = $scope.$new());
@@ -99,47 +101,108 @@ describe('accordion', function () {
ctrl.removeGroup({});
expect(ctrl.groups.length).toBe(2);
});
+ it('should remove a panel when the scope is destroyed', function() {
+ var group1, group2, group3;
+ ctrl.addGroup(group1 = $scope.$new());
+ ctrl.addGroup(group2 = $scope.$new());
+ ctrl.addGroup(group3 = $scope.$new());
+ group2.$destroy();
+ expect(ctrl.groups.length).toBe(2);
+ expect(ctrl.groups[0]).toBe(group1);
+ expect(ctrl.groups[1]).toBe(group3);
+ });
});
});
- describe('accordion-group', function () {
+ describe('uib-accordion', function() {
+ var scope, $compile, $templateCache, element;
+
+ beforeEach(inject(function($rootScope, _$compile_, _$templateCache_) {
+ scope = $rootScope;
+ $compile = _$compile_;
+ $templateCache = _$templateCache_;
+ }));
+ it('should be a tablist', function() {
+ element = $compile('')(scope);
+ scope.$digest();
+ expect(element.html()).toContain('role="tablist"');
+ });
+
+ it('should expose the controller on the view', function() {
+ $templateCache.put('uib/template/accordion/accordion.html', '
' +
+ '';
element = angular.element(tpl);
model = [
{name: 'title 1', content: 'Content 1'},
@@ -185,7 +330,7 @@ describe('accordion', function () {
scope.$digest();
});
- it('should have no panels initially', function () {
+ it('should have no panels initially', function() {
groups = element.find('.panel');
expect(groups.length).toEqual(0);
});
@@ -201,7 +346,7 @@ describe('accordion', function () {
expect(findGroupBody(1).text().trim()).toEqual('Content 2');
});
- it('should react properly on removing items from the model', function () {
+ it('should react properly on removing items from the model', function() {
scope.groups = model;
scope.$digest();
groups = element.find('.panel');
@@ -215,12 +360,12 @@ describe('accordion', function () {
});
describe('is-open attribute', function() {
- beforeEach(function () {
+ beforeEach(function() {
var tpl =
- '' +
- 'Content 1' +
- 'Content 2' +
- '';
+ '' +
+ '
Content 1
' +
+ '
Content 2
' +
+ '';
element = angular.element(tpl);
scope.open = { first: false, second: true };
$compile(element)(scope);
@@ -228,7 +373,7 @@ describe('accordion', function () {
groups = element.find('.panel');
});
- it('should open the panel with isOpen set to true', function () {
+ 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);
});
@@ -245,12 +390,12 @@ describe('accordion', function () {
});
describe('is-open attribute with dynamic content', function() {
- beforeEach(function () {
+ beforeEach(function() {
var tpl =
- '' +
- '
{{item}}
' +
- 'Static content' +
- '';
+ '' +
+ '
{{item}}
' +
+ '
Static content
' +
+ '';
element = angular.element(tpl);
scope.items = ['Item 1', 'Item 2', 'Item 3'];
scope.open1 = true;
@@ -258,6 +403,7 @@ describe('accordion', function () {
angular.element(document.body).append(element);
$compile(element)(scope);
scope.$digest();
+ $animate.flush();
groups = element.find('.panel');
});
@@ -265,18 +411,18 @@ describe('accordion', function () {
element.remove();
});
- it('should have visible panel body when the group with isOpen set to true', function () {
+ it('should have visible panel body when the group with isOpen set to true', function() {
expect(findGroupBody(0)).toHaveClass('in');
expect(findGroupBody(1)).not.toHaveClass('in');
});
});
- describe('is-open attribute with dynamic groups', function () {
- beforeEach(function () {
+ describe('is-open attribute with dynamic groups', function() {
+ beforeEach(function() {
var tpl =
- '' +
- '{{group.content}}' +
- '';
+ '' +
+ '
{{group.content}}
' +
+ '';
element = angular.element(tpl);
scope.groups = [
{name: 'title 1', content: 'Content 1', open: false},
@@ -288,7 +434,7 @@ describe('accordion', function () {
groups = element.find('.panel');
});
- it('should have visible group body when the group with isOpen set to true', function () {
+ 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);
});
@@ -306,13 +452,36 @@ describe('accordion', function () {
});
});
+ describe('is-open attribute with custom class', function() {
+ beforeEach(function() {
+ var tpl =
+ '' +
+ '
' +
+ '';
element = angular.element(tpl);
scope.disabled = true;
$compile(element)(scope);
@@ -321,7 +490,7 @@ describe('accordion', function () {
groupBody = findGroupBody(0);
});
- it('should open the panel with isOpen set to true', function () {
+ it('should open the panel with isOpen set to true', function() {
expect(groupBody.scope().isOpen).toBeFalsy();
});
@@ -340,55 +509,86 @@ describe('accordion', function () {
scope.$digest();
expect(groupBody.scope().isOpen).toBeTruthy();
});
+
+ it('should have text-muted styling', function() {
+ expect(findGroupLink(0).find('span:first')).toHaveClass('text-muted');
+ });
});
- describe('accordion-heading element', function() {
+ // This is re-used in both the uib-accordion-heading element and the uib-accordion-heading attribute tests
+ function isDisabledStyleCheck() {
+ var tpl =
+ '' +
+ '
' +
+ '';
element = $compile(tpl)(scope);
scope.$digest();
groups = element.find('.panel');
});
- it('transcludes the content into the heading link', function() {
+
+ it('transcludes the content into the heading link', function() {
expect(findGroupLink(0).text()).toBe('Heading Element 123 ');
});
+
it('attaches the same scope to the transcluded heading and body', function() {
- expect(findGroupLink(0).find('span').scope().$id).toBe(findGroupBody(0).find('span').scope().$id);
+ expect(findGroupLink(0).scope().$id).toBe(findGroupBody(0).scope().$id);
});
+ it('should wrap the transcluded content in a span', function() {
+ expect(findGroupLink(0).find('span:first').length).toEqual(1);
+ });
+
+ it('should have disabled styling when is-disabled is true', isDisabledStyleCheck);
});
- describe('accordion-heading attribute', function() {
+ describe('uib-accordion-heading attribute', function() {
beforeEach(function() {
var tpl =
- '' +
- '' +
- '
Heading Element {{x}}
' +
+ '' +
+ '
' +
+ '
Heading Element {{x}}
' +
'Body' +
- '' +
- '';
+ '
' +
+ '';
element = $compile(tpl)(scope);
scope.$digest();
groups = element.find('.panel');
});
- it('transcludes the content into the heading link', function() {
+
+ it('transcludes the content into the heading link', function() {
expect(findGroupLink(0).text()).toBe('Heading Element 123 ');
});
+
it('attaches the same scope to the transcluded heading and body', function() {
- expect(findGroupLink(0).find('span').scope().$id).toBe(findGroupBody(0).find('span').scope().$id);
+ expect(findGroupLink(0).scope().$id).toBe(findGroupBody(0).scope().$id);
});
+ it('should have disabled styling when is-disabled is true', isDisabledStyleCheck);
});
- describe('accordion-heading, with repeating accordion-groups', function() {
- it('should clone the accordion-heading for each group', function() {
- element = $compile('{{x}}')(scope);
+ describe('uib-accordion-heading, with repeating uib-accordion-groups', function() {
+ it('should clone the uib-accordion-heading for each group', function() {
+ element = $compile('
{{x}}
')(scope);
scope.$digest();
groups = element.find('.panel');
expect(groups.length).toBe(3);
@@ -398,10 +598,9 @@ describe('accordion', function () {
});
});
-
- describe('accordion-heading attribute, with repeating accordion-groups', function() {
- it('should clone the accordion-heading for each group', function() {
- element = $compile('
{{x}}
')(scope);
+ describe('uib-accordion-heading attribute, with repeating uib-accordion-groups', function() {
+ it('should clone the uib-accordion-heading for each group', function() {
+ element = $compile('
{{x}}
')(scope);
scope.$digest();
groups = element.find('.panel');
expect(groups.length).toBe(3);
@@ -411,5 +610,15 @@ describe('accordion', function () {
});
});
+ describe('uib-accordion-heading attribute, with custom template', function() {
+ it('should transclude heading to a template using data-uib-accordion-header', inject(function($templateCache) {
+ $templateCache.put('foo/bar.html', '
diff --git a/src/alert/docs/readme.md b/src/alert/docs/readme.md
index 4bc6f5242c..cf0bcad1ab 100644
--- a/src/alert/docs/readme.md
+++ b/src/alert/docs/readme.md
@@ -1,7 +1,15 @@
-Alert is an AngularJS-version of bootstrap's alert.
+This directive can be used both to generate alerts from static and dynamic model data (using the `ng-repeat` directive).
-This directive can be used to generate alerts from the dynamic model data (using the `ng-repeat` directive);
+### uib-alert settings
-The presence of the `close` attribute determines if a close button is displayed.
+* `close()`
+ $ -
+ A callback function that gets fired when an `alert` is closed. If the attribute exists, a close button is displayed as well.
-The optional `dismiss-on-timeout` attribute takes the number of milliseconds that specify timeout duration, after which the alert will be closed.
+* `dismiss-on-timeout`
+ _(Default: `none`)_ -
+ Takes the number of milliseconds that specify the timeout duration, after which the alert will be closed. This attribute requires the presence of the `close` attribute.
+
+* `template-url`
+ _(Default: `uib/template/alert/alert.html`)_ -
+ Add the ability to override the template used in the component.
diff --git a/src/alert/index.js b/src/alert/index.js
new file mode 100644
index 0000000000..f5a50975d1
--- /dev/null
+++ b/src/alert/index.js
@@ -0,0 +1,8 @@
+require('../../template/alert/alert.html.js');
+require('./alert');
+
+var MODULE_NAME = 'ui.bootstrap.module.alert';
+
+angular.module(MODULE_NAME, ['ui.bootstrap.alert', 'uib/template/alert/alert.html']);
+
+module.exports = MODULE_NAME;
diff --git a/src/alert/test/alert.spec.js b/src/alert/test/alert.spec.js
index 271833309a..bc87c7f7ad 100644
--- a/src/alert/test/alert.spec.js
+++ b/src/alert/test/alert.spec.js
@@ -1,21 +1,22 @@
-describe('alert', function () {
- var scope, $compile;
- var element;
+describe('uib-alert', function() {
+ var element, scope, $compile, $templateCache, $timeout;
beforeEach(module('ui.bootstrap.alert'));
- beforeEach(module('template/alert/alert.html'));
-
- beforeEach(inject(function ($rootScope, _$compile_) {
+ beforeEach(module('uib/template/alert/alert.html'));
+ beforeEach(inject(function($rootScope, _$compile_, _$templateCache_, _$timeout_) {
scope = $rootScope;
$compile = _$compile_;
+ $templateCache = _$templateCache_;
+ $timeout = _$timeout_;
element = angular.element(
- '
' +
- '{{alert.msg}}' +
- '' +
- '
');
+ '
' +
+ '
{{alert.msg}}' +
+ '
' +
+ '
');
scope.alerts = [
{ msg:'foo', type:'success'},
@@ -35,29 +36,36 @@ describe('alert', function () {
}
function findContent(index) {
- return element.find('div[ng-transclude] span').eq(index);
+ return element.find('div[ng-transclude]').eq(index);
}
- it('should generate alerts using ng-repeat', function () {
- var alerts = createAlerts();
- expect(alerts.length).toEqual(3);
- });
+ it('should expose the controller to the view', function() {
+ $templateCache.put('uib/template/alert/alert.html', '
{{alert.text}}
');
- 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');
+ element = $compile('')(scope);
+ scope.$digest();
+
+ var ctrl = element.controller('uib-alert');
+ expect(ctrl).toBeDefined();
+
+ ctrl.text = 'foo';
+ scope.$digest();
+
+ expect(element.html()).toBe('
foo
');
});
- it('should respect alert type binding', function () {
- var alerts = createAlerts();
- expect(alerts.eq(0)).toHaveClass('alert-success');
+ it('should support custom templates', function() {
+ $templateCache.put('foo/bar.html', '
');
+ });
+
+ it('should generate alerts using ng-repeat', function() {
+ var alerts = createAlerts();
+ expect(alerts.length).toEqual(3);
});
it('should show the alert content', function() {
@@ -68,7 +76,7 @@ describe('alert', function () {
}
});
- it('should show close buttons and have the dismissible class', function () {
+ it('should show close buttons and have the dismissible class', function() {
var alerts = createAlerts();
for (var i = 0, n = alerts.length; i < n; i++) {
@@ -77,11 +85,10 @@ describe('alert', function () {
}
});
- it('should fire callback when closed', function () {
-
+ it('should fire callback when closed', function() {
var alerts = createAlerts();
- scope.$apply(function () {
+ scope.$apply(function() {
scope.removeAlert = jasmine.createSpy();
});
@@ -91,18 +98,32 @@ describe('alert', function () {
expect(scope.removeAlert).toHaveBeenCalledWith(1);
});
- it('should not show close button and have the dismissible class if no close callback specified', function () {
- element = $compile('No close')(scope);
+ it('should not show close button and have the dismissible class if no close callback specified', function() {
+ element = $compile('
No close
')(scope);
scope.$digest();
expect(findCloseButton(0)).toBeHidden();
expect(element).not.toHaveClass('alert-dismissible');
});
- it('should be possible to add additional classes for alert', function () {
- var element = $compile('Default alert!')(scope);
+ it('should close automatically if dismiss-on-timeout is defined on the element', function() {
+ scope.removeAlert = jasmine.createSpy();
+ $compile('
Default alert!
')(scope);
scope.$digest();
- expect(element).toHaveClass('alert-block');
- expect(element).toHaveClass('alert-info');
+
+ $timeout.flush();
+ expect(scope.removeAlert).toHaveBeenCalled();
});
+ it('should not close immediately with a dynamic dismiss-on-timeout', function() {
+ scope.removeAlert = jasmine.createSpy();
+ scope.dismissTime = 500;
+ $compile('
+
diff --git a/src/buttons/docs/demo.js b/src/buttons/docs/demo.js
index 3cda78a7a8..3d5760fbec 100644
--- a/src/buttons/docs/demo.js
+++ b/src/buttons/docs/demo.js
@@ -8,4 +8,15 @@ angular.module('ui.bootstrap.demo').controller('ButtonsCtrl', function ($scope)
middle: true,
right: false
};
+
+ $scope.checkResults = [];
+
+ $scope.$watchCollection('checkModel', function () {
+ $scope.checkResults = [];
+ angular.forEach($scope.checkModel, function (value, key) {
+ if (value) {
+ $scope.checkResults.push(key);
+ }
+ });
+ });
});
\ No newline at end of file
diff --git a/src/buttons/docs/readme.md b/src/buttons/docs/readme.md
index 82e736b107..e5ff9629bb 100644
--- a/src/buttons/docs/readme.md
+++ b/src/buttons/docs/readme.md
@@ -1 +1,50 @@
-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.
+With the buttons directive, we can make a group of buttons behave like a set of checkboxes (`uib-btn-checkbox`) or behave like a set of radio buttons (`uib-btn-radio`).
+
+### uib-btn-checkbox settings
+
+* `btn-checkbox-false`
+ _(Default: `false`)_ -
+ Sets the value for the unchecked status.
+
+* `btn-checkbox-true`
+ _(Default: `true`)_ -
+ Sets the value for the checked status.
+
+* `ng-model`
+ $
+ -
+ Model where we set the checkbox status. By default `true` or `false`.
+
+### uib-btn-radio settings
+
+* `ng-model`
+ $
+ -
+ Model where we set the radio status. All radio buttons in a group should use the same `ng-model`.
+
+* `uib-btn-radio` -
+ $
+ Value to assign to the `ng-model` if we check this radio button.
+
+* `uib-uncheckable`
+ $
+ _(Default: `null`)_ -
+ An expression that evaluates to a truthy or falsy value that determines whether the `uncheckable` attribute is present.
+
+* `uncheckable`
+ B -
+ Whether a radio button can be unchecked or not.
+
+### Additional settings `uibButtonConfig`
+
+* `activeClass`
+ _(Default: `active`)_ -
+ Class to apply to the checked buttons.
+
+* `toggleEvent`
+ _(Default: `click`)_ -
+ Event used to toggle the buttons.
+
+### Known issues
+
+To use tooltips or popovers on elements within a `btn-group`, set the tooltip/popover `appendToBody` option to `true`. This is due to Bootstrap CSS styling. See [here](http://getbootstrap.com/components/#btn-groups) for more information.
diff --git a/src/buttons/index.js b/src/buttons/index.js
new file mode 100644
index 0000000000..181f258adc
--- /dev/null
+++ b/src/buttons/index.js
@@ -0,0 +1,7 @@
+require('./buttons');
+
+var MODULE_NAME = 'ui.bootstrap.module.buttons';
+
+angular.module(MODULE_NAME, ['ui.bootstrap.buttons']);
+
+module.exports = MODULE_NAME;
diff --git a/src/buttons/test/buttons.spec.js b/src/buttons/test/buttons.spec.js
index f1ef6602f3..de2945f45a 100644
--- a/src/buttons/test/buttons.spec.js
+++ b/src/buttons/test/buttons.spec.js
@@ -1,25 +1,35 @@
-describe('buttons', function () {
+describe('buttons', function() {
var $scope, $compile;
beforeEach(module('ui.bootstrap.buttons'));
- beforeEach(inject(function (_$rootScope_, _$compile_) {
+ beforeEach(inject(function(_$rootScope_, _$compile_) {
$scope = _$rootScope_;
$compile = _$compile_;
}));
- describe('checkbox', function () {
-
- var compileButton = function (markup, scope) {
+ describe('checkbox', function() {
+ var compileButton = function(markup, scope) {
var el = $compile(markup)(scope);
scope.$digest();
return el;
};
+ it('should expose the controller to the view', inject(function($templateCache) {
+ var btn = compileButton('{{button.text}}', $scope);
+ var ctrl = btn.controller('uibBtnCheckbox');
+ expect(ctrl).toBeDefined();
+
+ ctrl.text = 'foo';
+ $scope.$digest();
+
+ expect(btn.html()).toBe('foo');
+ }));
+
//model -> UI
- it('should work correctly with default model values', function () {
+ it('should work correctly with default model values', function() {
$scope.model = false;
- var btn = compileButton('click', $scope);
+ var btn = compileButton('click', $scope);
expect(btn).not.toHaveClass('active');
$scope.model = true;
@@ -27,9 +37,9 @@ describe('buttons', function () {
expect(btn).toHaveClass('active');
});
- it('should bind custom model values', function () {
+ it('should bind custom model values', function() {
$scope.model = 1;
- var btn = compileButton('click', $scope);
+ var btn = compileButton('click', $scope);
expect(btn).toHaveClass('active');
$scope.model = 0;
@@ -38,9 +48,9 @@ describe('buttons', function () {
});
//UI-> model
- it('should toggle default model values on click', function () {
+ it('should toggle default model values on click', function() {
$scope.model = false;
- var btn = compileButton('click', $scope);
+ var btn = compileButton('click', $scope);
btn.click();
expect($scope.model).toEqual(true);
@@ -51,9 +61,9 @@ describe('buttons', function () {
expect(btn).not.toHaveClass('active');
});
- it('should toggle custom model values on click', function () {
+ it('should toggle custom model values on click', function() {
$scope.model = 0;
- var btn = compileButton('click', $scope);
+ var btn = compileButton('click', $scope);
btn.click();
expect($scope.model).toEqual(1);
@@ -64,11 +74,11 @@ describe('buttons', function () {
expect(btn).not.toHaveClass('active');
});
- it('should monitor true / false value changes - issue 666', function () {
+ it('should monitor true / false value changes - issue 666', function() {
$scope.model = 1;
$scope.trueVal = 1;
- var btn = compileButton('click', $scope);
+ var btn = compileButton('click', $scope);
expect(btn).toHaveClass('active');
expect($scope.model).toEqual(1);
@@ -81,46 +91,97 @@ describe('buttons', function () {
expect($scope.model).toEqual(2);
});
- describe('setting buttonConfig', function () {
- var originalActiveClass, originalToggleEvent;
+ it('should not toggle when disabled - issue 4013', function() {
+ $scope.model = 1;
+ $scope.falseVal = 0;
+ var btn = compileButton('click', $scope);
+
+ expect(btn).not.toHaveClass('active');
+ expect($scope.model).toEqual(1);
+
+ btn.click();
+
+ expect(btn).not.toHaveClass('active');
+
+ $scope.$digest();
+
+ expect(btn).not.toHaveClass('active');
+ });
+
+ describe('setting buttonConfig', function() {
+ var uibButtonConfig, originalActiveClass, originalToggleEvent;
- beforeEach(inject(function(buttonConfig) {
- originalActiveClass = buttonConfig.activeClass;
- originalToggleEvent = buttonConfig.toggleEvent;
- buttonConfig.activeClass = false;
- buttonConfig.toggleEvent = false;
+ beforeEach(inject(function(_uibButtonConfig_) {
+ uibButtonConfig = _uibButtonConfig_;
+ originalActiveClass = uibButtonConfig.activeClass;
+ originalToggleEvent = uibButtonConfig.toggleEvent;
+ uibButtonConfig.activeClass = false;
+ uibButtonConfig.toggleEvent = false;
}));
- afterEach(inject(function(buttonConfig) {
+ afterEach(function() {
// return it to the original value
- buttonConfig.activeClass = originalActiveClass;
- buttonConfig.toggleEvent = originalToggleEvent;
- }));
+ uibButtonConfig.activeClass = originalActiveClass;
+ uibButtonConfig.toggleEvent = originalToggleEvent;
+ });
- it('should use default config when buttonConfig.activeClass and buttonConfig.toggleEvent is false', function () {
+ it('should use default config when buttonConfig.activeClass and buttonConfig.toggleEvent is false', function() {
$scope.model = false;
- var btn = compileButton('click', $scope);
+ var btn = compileButton('click', $scope);
expect(btn).not.toHaveClass('active');
$scope.model = true;
$scope.$digest();
expect(btn).toHaveClass('active');
});
+
+ it('should be able to use a different active class', function() {
+ uibButtonConfig.activeClass = 'foo';
+ $scope.model = false;
+ var btn = compileButton('click', $scope);
+ expect(btn).not.toHaveClass('foo');
+
+ $scope.model = true;
+ $scope.$digest();
+ expect(btn).toHaveClass('foo');
+ });
+
+ it('should be able to use a different toggle event', function() {
+ uibButtonConfig.toggleEvent = 'mouseenter';
+ $scope.model = false;
+ var btn = compileButton('click', $scope);
+ expect(btn).not.toHaveClass('active');
+
+ btn.trigger('mouseenter');
+
+ $scope.$digest();
+ expect(btn).toHaveClass('active');
+ });
});
});
- describe('radio', function () {
-
- var compileButtons = function (markup, scope) {
+ describe('radio', function() {
+ var compileButtons = function(markup, scope) {
var el = $compile('
'+markup+'
')(scope);
scope.$digest();
return el.find('button');
};
+ it('should expose the controller to the view', inject(function($templateCache) {
+ var btn = compileButtons('{{buttons.text}}', $scope);
+ var ctrl = btn.controller('uibBtnRadio');
+ expect(ctrl).toBeDefined();
+
+ ctrl.text = 'foo';
+ $scope.$digest();
+
+ expect(btn.html()).toBe('foo');
+ }));
+
//model -> UI
- it('should work correctly set active class based on model', function () {
- var btns = compileButtons('click1click2', $scope);
+ it('should set active class based on model', function() {
+ var btns = compileButtons('click1click2', $scope);
expect(btns.eq(0)).not.toHaveClass('active');
expect(btns.eq(1)).not.toHaveClass('active');
@@ -131,8 +192,8 @@ describe('buttons', function () {
});
//UI->model
- it('should work correctly set active class based on model', function () {
- var btns = compileButtons('click1click2', $scope);
+ it('should set active class via click', function() {
+ var btns = compileButtons('click1click2', $scope);
expect($scope.model).toBeUndefined();
btns.eq(0).click();
@@ -146,10 +207,10 @@ describe('buttons', function () {
expect(btns.eq(0)).not.toHaveClass('active');
});
- it('should watch btn-radio values and update state accordingly', function () {
+ it('should watch uib-btn-radio values and update state accordingly', function() {
$scope.values = ['value1', 'value2'];
- var btns = compileButtons('click1click2', $scope);
+ var btns = compileButtons('click1click2', $scope);
expect(btns.eq(0)).not.toHaveClass('active');
expect(btns.eq(1)).not.toHaveClass('active');
@@ -165,22 +226,59 @@ describe('buttons', function () {
expect(btns.eq(1)).toHaveClass('active');
});
- it('should do nothing when click active radio', function () {
+ it('should do nothing when clicking an active radio', function() {
+ $scope.model = 1;
+ var btns = compileButtons('click1click2', $scope);
+ expect(btns.eq(0)).toHaveClass('active');
+ expect(btns.eq(1)).not.toHaveClass('active');
+
+ btns.eq(0).click();
+ $scope.$digest();
+ expect(btns.eq(0)).toHaveClass('active');
+ expect(btns.eq(1)).not.toHaveClass('active');
+ });
+
+ it('should not toggle when disabled - issue 4013', function() {
$scope.model = 1;
- var btns = compileButtons('click1click2', $scope);
+ var btns = compileButtons('click1click2', $scope);
+
+ expect(btns.eq(0)).toHaveClass('active');
+ expect(btns.eq(1)).not.toHaveClass('active');
+
+ btns.eq(1).click();
+
+ expect(btns.eq(0)).toHaveClass('active');
+ expect(btns.eq(1)).not.toHaveClass('active');
+
+ $scope.$digest();
+
expect(btns.eq(0)).toHaveClass('active');
expect(btns.eq(1)).not.toHaveClass('active');
+ });
+
+ it('should handle string values in uib-btn-radio value', function() {
+ $scope.model = 'Two';
+ var btns = compileButtons('click1click2', $scope);
+
+ expect(btns.eq(0)).not.toHaveClass('active');
+ expect(btns.eq(1)).toHaveClass('active');
btns.eq(0).click();
+ expect(btns.eq(0)).toHaveClass('active');
+ expect(btns.eq(1)).not.toHaveClass('active');
+ expect($scope.model).toEqual('One');
+
$scope.$digest();
+
expect(btns.eq(0)).toHaveClass('active');
expect(btns.eq(1)).not.toHaveClass('active');
+ expect($scope.model).toEqual('One');
});
- describe('uncheckable', function () {
+ describe('uncheckable', function() {
//model -> UI
- it('should set active class based on model', function () {
- var btns = compileButtons('click1click2', $scope);
+ it('should set active class based on model', function() {
+ var btns = compileButtons('click1click2', $scope);
expect(btns.eq(0)).not.toHaveClass('active');
expect(btns.eq(1)).not.toHaveClass('active');
@@ -191,8 +289,8 @@ describe('buttons', function () {
});
//UI->model
- it('should unset active class based on model', function () {
- var btns = compileButtons('click1click2', $scope);
+ it('should unset active class via click', function() {
+ var btns = compileButtons('click1click2', $scope);
expect($scope.model).toBeUndefined();
btns.eq(0).click();
@@ -206,10 +304,10 @@ describe('buttons', function () {
expect(btns.eq(0)).not.toHaveClass('active');
});
- it('should watch btn-radio values and update state', function () {
+ it('should watch uib-btn-radio values and update state', function() {
$scope.values = ['value1', 'value2'];
- var btns = compileButtons('click1click2', $scope);
+ var btns = compileButtons('click1click2', $scope);
expect(btns.eq(0)).not.toHaveClass('active');
expect(btns.eq(1)).not.toHaveClass('active');
@@ -224,5 +322,45 @@ describe('buttons', function () {
expect(btns.eq(1)).not.toHaveClass('active');
});
});
+
+ describe('uibUncheckable', function() {
+ it('should set uncheckable', function() {
+ $scope.uncheckable = false;
+ var btns = compileButtons('click1click2', $scope);
+ expect(btns.eq(0).attr('uncheckable')).toBeUndefined();
+ expect(btns.eq(1).attr('uncheckable')).toBeUndefined();
+
+ expect($scope.model).toBeUndefined();
+
+ btns.eq(0).click();
+ expect($scope.model).toEqual(1);
+
+ btns.eq(0).click();
+ expect($scope.model).toEqual(1);
+
+ btns.eq(1).click();
+ expect($scope.model).toEqual(2);
+
+ btns.eq(1).click();
+ expect($scope.model).toEqual(2);
+
+ $scope.uncheckable = true;
+ $scope.$digest();
+ expect(btns.eq(0).attr('uncheckable')).toBeUndefined();
+ expect(btns.eq(1).attr('uncheckable')).toBeDefined();
+
+ btns.eq(0).click();
+ expect($scope.model).toEqual(1);
+
+ btns.eq(0).click();
+ expect($scope.model).toEqual(1);
+
+ btns.eq(1).click();
+ expect($scope.model).toEqual(2);
+
+ btns.eq(1).click();
+ expect($scope.model).toBeNull();
+ });
+ });
});
});
diff --git a/src/carousel/carousel.js b/src/carousel/carousel.js
index ce50c9429a..aaa04f8a95 100644
--- a/src/carousel/carousel.js
+++ b/src/carousel/carousel.js
@@ -1,106 +1,229 @@
-/**
-* @ngdoc overview
-* @name ui.bootstrap.carousel
-*
-* @description
-* AngularJS version of an image carousel.
-*
-*/
angular.module('ui.bootstrap.carousel', [])
-.controller('CarouselController', ['$scope', '$interval', '$animate', function ($scope, $interval, $animate) {
+
+.controller('UibCarouselController', ['$scope', '$element', '$interval', '$timeout', '$animate', function($scope, $element, $interval, $timeout, $animate) {
var self = this,
slides = self.slides = $scope.slides = [],
- currentIndex = -1,
+ SLIDE_DIRECTION = 'uib-slideDirection',
+ currentIndex = $scope.active,
currentInterval, isPlaying;
- self.currentSlide = null;
var destroyed = false;
+ $element.addClass('carousel');
+
+ self.addSlide = function(slide, element) {
+ slides.push({
+ slide: slide,
+ element: element
+ });
+ slides.sort(function(a, b) {
+ return +a.slide.index - +b.slide.index;
+ });
+ //if this is the first slide or the slide is set to active, select it
+ if (slide.index === $scope.active || slides.length === 1 && !angular.isNumber($scope.active)) {
+ if ($scope.$currentTransition) {
+ $scope.$currentTransition = null;
+ }
+
+ currentIndex = slide.index;
+ $scope.active = slide.index;
+ setActive(currentIndex);
+ self.select(slides[findSlideIndex(slide)]);
+ if (slides.length === 1) {
+ $scope.play();
+ }
+ }
+ };
+
+ self.getCurrentIndex = function() {
+ for (var i = 0; i < slides.length; i++) {
+ if (slides[i].slide.index === currentIndex) {
+ return i;
+ }
+ }
+ };
+
+ self.next = $scope.next = function() {
+ var newIndex = (self.getCurrentIndex() + 1) % slides.length;
+
+ if (newIndex === 0 && $scope.noWrap()) {
+ $scope.pause();
+ return;
+ }
+
+ return self.select(slides[newIndex], 'next');
+ };
+
+ self.prev = $scope.prev = function() {
+ var newIndex = self.getCurrentIndex() - 1 < 0 ? slides.length - 1 : self.getCurrentIndex() - 1;
+
+ if ($scope.noWrap() && newIndex === slides.length - 1) {
+ $scope.pause();
+ return;
+ }
+
+ return self.select(slides[newIndex], 'prev');
+ };
+
+ self.removeSlide = function(slide) {
+ var index = findSlideIndex(slide);
+
+ //get the index of the slide inside the carousel
+ slides.splice(index, 1);
+ if (slides.length > 0 && currentIndex === index) {
+ if (index >= slides.length) {
+ currentIndex = slides.length - 1;
+ $scope.active = currentIndex;
+ setActive(currentIndex);
+ self.select(slides[slides.length - 1]);
+ } else {
+ currentIndex = index;
+ $scope.active = currentIndex;
+ setActive(currentIndex);
+ self.select(slides[index]);
+ }
+ } else if (currentIndex > index) {
+ currentIndex--;
+ $scope.active = currentIndex;
+ }
+
+ //clean the active value when no more slide
+ if (slides.length === 0) {
+ currentIndex = null;
+ $scope.active = null;
+ }
+ };
+
/* direction: "prev" or "next" */
self.select = $scope.select = function(nextSlide, direction) {
- var nextIndex = self.indexOfSlide(nextSlide);
+ var nextIndex = findSlideIndex(nextSlide.slide);
//Decide direction if it's not given
if (direction === undefined) {
direction = nextIndex > self.getCurrentIndex() ? 'next' : 'prev';
}
//Prevent this user-triggered transition from occurring if there is already one in progress
- if (nextSlide && nextSlide !== self.currentSlide && !$scope.$currentTransition) {
- goNext(nextSlide, nextIndex, direction);
+ if (nextSlide.slide.index !== currentIndex &&
+ !$scope.$currentTransition) {
+ goNext(nextSlide.slide, nextIndex, direction);
}
};
- function goNext(slide, index, direction) {
- // Scope has been destroyed, stop here.
- if (destroyed) { return; }
+ /* Allow outside people to call indexOf on slides array */
+ $scope.indexOfSlide = function(slide) {
+ return +slide.slide.index;
+ };
- angular.extend(slide, {direction: direction, active: true});
- angular.extend(self.currentSlide || {}, {direction: direction, active: false});
- if ($animate.enabled() && !$scope.noTransition && !$scope.$currentTransition &&
- slide.$element) {
- $scope.$currentTransition = true;
- slide.$element.one('$animate:close', function closeFn() {
- $scope.$currentTransition = null;
- });
+ $scope.isActive = function(slide) {
+ return $scope.active === slide.slide.index;
+ };
+
+ $scope.isPrevDisabled = function() {
+ return $scope.active === 0 && $scope.noWrap();
+ };
+
+ $scope.isNextDisabled = function() {
+ return $scope.active === slides.length - 1 && $scope.noWrap();
+ };
+
+ $scope.pause = function() {
+ if (!$scope.noPause) {
+ isPlaying = false;
+ resetTimer();
}
+ };
- self.currentSlide = slide;
- currentIndex = index;
+ $scope.play = function() {
+ if (!isPlaying) {
+ isPlaying = true;
+ restartTimer();
+ }
+ };
- //every time you change slides, reset the timer
- restartTimer();
- }
+ $element.on('mouseenter', $scope.pause);
+ $element.on('mouseleave', $scope.play);
- $scope.$on('$destroy', function () {
+ $scope.$on('$destroy', function() {
destroyed = true;
+ resetTimer();
});
- function getSlideByIndex(index) {
- if (angular.isUndefined(slides[index].index)) {
- return slides[index];
+ $scope.$watch('noTransition', function(noTransition) {
+ $animate.enabled($element, !noTransition);
+ });
+
+ $scope.$watch('interval', restartTimer);
+
+ $scope.$watchCollection('slides', resetTransition);
+
+ $scope.$watch('active', function(index) {
+ if (angular.isNumber(index) && currentIndex !== index) {
+ for (var i = 0; i < slides.length; i++) {
+ if (slides[i].slide.index === index) {
+ index = i;
+ break;
+ }
+ }
+
+ var slide = slides[index];
+ if (slide) {
+ setActive(index);
+ self.select(slides[index]);
+ currentIndex = index;
+ }
}
- var i, len = slides.length;
- for (i = 0; i < slides.length; ++i) {
- if (slides[i].index == index) {
+ });
+
+ function getSlideByIndex(index) {
+ for (var i = 0, l = slides.length; i < l; ++i) {
+ if (slides[i].index === index) {
return slides[i];
}
}
}
- self.getCurrentIndex = function() {
- if (self.currentSlide && angular.isDefined(self.currentSlide.index)) {
- return +self.currentSlide.index;
+ function setActive(index) {
+ for (var i = 0; i < slides.length; i++) {
+ slides[i].slide.active = i === index;
}
- return currentIndex;
- };
-
- /* Allow outside people to call indexOf on slides array */
- self.indexOfSlide = function(slide) {
- return angular.isDefined(slide.index) ? +slide.index : slides.indexOf(slide);
- };
+ }
- $scope.next = function() {
- var newIndex = (self.getCurrentIndex() + 1) % slides.length;
+ function goNext(slide, index, direction) {
+ if (destroyed) {
+ return;
+ }
- return self.select(getSlideByIndex(newIndex), 'next');
- };
+ angular.extend(slide, {direction: direction});
+ angular.extend(slides[currentIndex].slide || {}, {direction: direction});
+ if ($animate.enabled($element) && !$scope.$currentTransition &&
+ slides[index].element && self.slides.length > 1) {
+ slides[index].element.data(SLIDE_DIRECTION, slide.direction);
+ var currentIdx = self.getCurrentIndex();
- $scope.prev = function() {
- var newIndex = self.getCurrentIndex() - 1 < 0 ? slides.length - 1 : self.getCurrentIndex() - 1;
+ if (angular.isNumber(currentIdx) && slides[currentIdx].element) {
+ slides[currentIdx].element.data(SLIDE_DIRECTION, slide.direction);
+ }
- return self.select(getSlideByIndex(newIndex), 'prev');
- };
+ $scope.$currentTransition = true;
+ $animate.on('addClass', slides[index].element, function(element, phase) {
+ if (phase === 'close') {
+ $scope.$currentTransition = null;
+ $animate.off('addClass', element);
+ }
+ });
+ }
- $scope.isActive = function(slide) {
- return self.currentSlide === slide;
- };
+ $scope.active = slide.index;
+ currentIndex = slide.index;
+ setActive(index);
- $scope.$watch('interval', restartTimer);
- $scope.$on('$destroy', resetTimer);
+ //every time you change slides, reset the timer
+ restartTimer();
+ }
- function restartTimer() {
- resetTimer();
- var interval = +$scope.interval;
- if (!isNaN(interval) && interval > 0) {
- currentInterval = $interval(timerFn, interval);
+ function findSlideIndex(slide) {
+ for (var i = 0; i < slides.length; i++) {
+ if (slides[i].slide === slide) {
+ return i;
+ }
}
}
@@ -111,172 +234,63 @@ angular.module('ui.bootstrap.carousel', [])
}
}
- function timerFn() {
- var interval = +$scope.interval;
- if (isPlaying && !isNaN(interval) && interval > 0) {
- $scope.next();
- } else {
- $scope.pause();
+ function resetTransition(slides) {
+ if (!slides.length) {
+ $scope.$currentTransition = null;
}
}
- $scope.play = function() {
- if (!isPlaying) {
- isPlaying = true;
- restartTimer();
- }
- };
- $scope.pause = function() {
- if (!$scope.noPause) {
- isPlaying = false;
- resetTimer();
+ function restartTimer() {
+ resetTimer();
+ var interval = +$scope.interval;
+ if (!isNaN(interval) && interval > 0) {
+ currentInterval = $interval(timerFn, interval);
}
- };
+ }
- 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();
- }
+ function timerFn() {
+ var interval = +$scope.interval;
+ if (isPlaying && !isNaN(interval) && interval > 0 && slides.length) {
+ $scope.next();
} else {
- slide.active = false;
- }
- };
-
- self.removeSlide = function(slide) {
- if (angular.isDefined(slide.index)) {
- slides.sort(function(a, b) {
- return +a.index > +b.index;
- });
- }
- //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--;
+ $scope.pause();
}
- };
-
+ }
}])
-/**
- * @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
-
-
-
-
-
-
-
Beautiful!
-
-
-
-
-
-
D'aww!
-
-
-
-
-
- .carousel-indicators {
- top: auto;
- bottom: 15px;
- }
-
-
- */
-.directive('carousel', [function() {
+.directive('uibCarousel', function() {
return {
- restrict: 'EA',
transclude: true,
- replace: true,
- controller: 'CarouselController',
- require: 'carousel',
- templateUrl: 'template/carousel/carousel.html',
+ controller: 'UibCarouselController',
+ controllerAs: 'carousel',
+ restrict: 'A',
+ templateUrl: function(element, attrs) {
+ return attrs.templateUrl || 'uib/template/carousel/carousel.html';
+ },
scope: {
+ active: '=',
interval: '=',
noTransition: '=',
- noPause: '='
+ noPause: '=',
+ noWrap: '&'
}
};
-}])
-
-/**
- * @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.
- * @param {number=} index The index of the slide. The slides will be sorted by this parameter.
- *
- * @example
-
-
-
-
-
-
-
-
Slide {{$index}}
-
{{slide.text}}
-
-
-
- Interval, in milliseconds:
- Enter a negative number to stop the interval.
-
-
-
-function CarouselDemoCtrl($scope) {
- $scope.myInterval = 5000;
-}
-
-
- .carousel-indicators {
- top: auto;
- bottom: 15px;
- }
-
-
-*/
+})
-.directive('slide', function() {
+.directive('uibSlide', ['$animate', function($animate) {
return {
- require: '^carousel',
- restrict: 'EA',
+ require: '^uibCarousel',
+ restrict: 'A',
transclude: true,
- replace: true,
- templateUrl: 'template/carousel/slide.html',
+ templateUrl: function(element, attrs) {
+ return attrs.templateUrl || 'uib/template/carousel/slide.html';
+ },
scope: {
- active: '=?',
+ actual: '=?',
index: '=?'
},
link: function (scope, element, attrs, carouselCtrl) {
+ element.addClass('item');
carouselCtrl.addSlide(scope, element);
//when the scope is destroyed then remove the slide from the current slides array
scope.$on('$destroy', function() {
@@ -284,61 +298,59 @@ function CarouselDemoCtrl($scope) {
});
scope.$watch('active', function(active) {
- if (active) {
- carouselCtrl.select(scope);
- }
+ $animate[active ? 'addClass' : 'removeClass'](element, 'active');
});
}
};
-})
+}])
+
+.animation('.item', ['$animateCss',
+function($animateCss) {
+ var SLIDE_DIRECTION = 'uib-slideDirection';
+
+ function removeClass(element, className, callback) {
+ element.removeClass(className);
+ if (callback) {
+ callback();
+ }
+ }
-.animation('.item', [
- '$animate',
-function ($animate) {
return {
- beforeAddClass: function (element, className, done) {
- // Due to transclusion, noTransition property is on parent's scope
- if (className == 'active' && element.parent() &&
- !element.parent().scope().noTransition) {
+ beforeAddClass: function(element, className, done) {
+ if (className === 'active') {
var stopped = false;
- var direction = element.isolateScope().direction;
- var directionClass = direction == 'next' ? 'left' : 'right';
+ var direction = element.data(SLIDE_DIRECTION);
+ var directionClass = direction === 'next' ? 'left' : 'right';
+ var removeClassFn = removeClass.bind(this, element,
+ directionClass + ' ' + direction, done);
element.addClass(direction);
- $animate.addClass(element, directionClass).then(function () {
- if (!stopped) {
- element.removeClass(directionClass + ' ' + direction);
- }
- done();
- });
-
- return function () {
+
+ $animateCss(element, {addClass: directionClass})
+ .start()
+ .done(removeClassFn);
+
+ return function() {
stopped = true;
};
}
done();
},
beforeRemoveClass: function (element, className, done) {
- // Due to transclusion, noTransition property is on parent's scope
- if (className == 'active' && element.parent() &&
- !element.parent().scope().noTransition) {
+ if (className === 'active') {
var stopped = false;
- var direction = element.isolateScope().direction;
- var directionClass = direction == 'next' ? 'left' : 'right';
- $animate.addClass(element, directionClass).then(function () {
- if (!stopped) {
- element.removeClass(directionClass);
- }
- done();
- });
- return function () {
+ var direction = element.data(SLIDE_DIRECTION);
+ var directionClass = direction === 'next' ? 'left' : 'right';
+ var removeClassFn = removeClass.bind(this, element, directionClass, done);
+
+ $animateCss(element, {addClass: directionClass})
+ .start()
+ .done(removeClassFn);
+
+ return function() {
stopped = true;
};
}
done();
}
};
-
-}])
-
-
-;
+}]);
diff --git a/src/carousel/docs/README.md b/src/carousel/docs/README.md
index 2c7d173adc..80a883a888 100644
--- a/src/carousel/docs/README.md
+++ b/src/carousel/docs/README.md
@@ -2,4 +2,56 @@ Carousel creates a carousel similar to bootstrap's image carousel.
The carousel also offers support for touchscreen devices in the form of swiping. To enable swiping, load the `ngTouch` module as a dependency.
-Use a `` element with `` elements inside it. 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.
+Use a `` element with `` elements inside it.
+
+### uib-carousel settings
+
+* `active`
+
+ _(Default: `Index of first slide`)_ -
+ Index of current active slide.
+
+* `interval`
+ $
+
+ _(Default: `none`)_ -
+ Sets an interval to cycle through the slides. You need a number bigger than 0 to make the interval work.
+
+* `no-pause`
+ $
+
+ _(Default: `false`)_ -
+ The interval pauses on mouseover. Setting this to truthy, disables this pause.
+
+* `no-transition`
+ $
+
+ _(Default: `false`)_ -
+ Whether to disable the transition animation between slides. Setting this to truthy, disables this transition.
+
+* `no-wrap`
+ $
+ _(Default: `false`)_ -
+ Disables the looping of slides. Setting `no-wrap` to an expression which evaluates to a truthy value will prevent looping.
+
+* `template-url`
+ _(Default: `uib/template/carousel/carousel.html`)_ -
+ Add the ability to override the template used on the component.
+
+### uib-slide settings
+
+* `actual`
+ $
+
+ _(Default: `none`)_ -
+ Use this attribute to bind the slide model (or any object of interest) onto the slide scope, which makes it available for customization in the carousel template.
+
+* `index`
+ $
+
+ _(Default: `none`)_ -
+ The index of the slide. Must be unique.
+
+* `template-url`
+ _(Default: `uib/template/carousel/slide.html`)_ -
+ Add the ability to override the template used on the component.
diff --git a/src/carousel/docs/demo.html b/src/carousel/docs/demo.html
index 3f7d13b25b..f2a6f48672 100644
--- a/src/carousel/docs/demo.html
+++ b/src/carousel/docs/demo.html
@@ -1,18 +1,25 @@
'
+ )(scope);
var indicators = elm.find('ol.carousel-indicators > li');
expect(indicators.length).toBe(0);
@@ -93,6 +151,38 @@ describe('carousel', function() {
expect(navPrev.length).toBe(0);
});
+ it('should disable prev button when slide index is 0 and noWrap is truthy', function() {
+ scope.$apply();
+
+ var $scope = elm.isolateScope();
+ $scope.noWrap = function() {return true;};
+
+ $scope.isPrevDisabled();
+ scope.$apply();
+
+ var navPrev = elm.find('a.left');
+ expect(navPrev.hasClass('disabled')).toBe(true);
+ });
+
+ it('should disable next button when last slide is active and noWrap is truthy', function() {
+ scope.slides = [
+ {content: 'one', index: 0},
+ {content: 'two', index: 1}
+ ];
+
+ scope.$apply();
+
+ var $scope = elm.isolateScope();
+ $scope.noWrap = function() {return true;};
+ $scope.next();
+
+ $scope.isNextDisabled();
+ scope.$apply();
+
+ var navNext = elm.find('a.right');
+ expect(navNext.hasClass('disabled')).toBe(true);
+ });
+
it('should show navigation when there are 3 slides', function () {
var indicators = elm.find('ol.carousel-indicators > li');
expect(indicators.length).not.toBe(0);
@@ -126,27 +216,13 @@ describe('carousel', function() {
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() {
+ it('shouldnt go forward if interval is NaN or negative or has no slides', function() {
testSlideActive(0);
var previousInterval = scope.interval;
scope.$apply('interval = -1');
@@ -161,10 +237,13 @@ describe('carousel', function() {
scope.$apply('interval = 1000');
$interval.flush(1000);
testSlideActive(2);
+ scope.$apply('slides = []');
+ $interval.flush(1000);
+ testSlideActive(2);
});
it('should bind the content to slides', function() {
- var contents = elm.find('div.item');
+ var contents = elm.find('div.item [ng-transclude]');
expect(contents.length).toBe(3);
expect(contents.eq(0).text()).toBe('one');
@@ -217,22 +296,28 @@ describe('carousel', function() {
});
it('should remove slide from dom and change active slide', function() {
- scope.$apply('slides[2].active = true');
+ scope.$apply('active = 2');
testSlideActive(2);
- scope.$apply('slides.splice(0,1)');
+ scope.$apply('slides.splice(2,1)');
+ $timeout.flush(0);
expect(elm.find('div.item').length).toBe(2);
testSlideActive(1);
$interval.flush(scope.interval);
testSlideActive(0);
scope.$apply('slides.splice(1,1)');
+ $timeout.flush(0);
expect(elm.find('div.item').length).toBe(1);
testSlideActive(0);
});
it('should change dom when you reassign ng-repeat slides array', function() {
- scope.slides=[{content:'new1'},{content:'new2'},{content:'new3'}];
+ scope.slides = [
+ {content:'new1', index: 4},
+ {content:'new2', index: 5},
+ {content:'new3', index: 6}
+ ];
scope.$apply();
- var contents = elm.find('div.item');
+ var contents = elm.find('div.item [ng-transclude]');
expect(contents.length).toBe(3);
expect(contents.eq(0).text()).toBe('new1');
expect(contents.eq(1).text()).toBe('new2');
@@ -254,6 +339,39 @@ describe('carousel', function() {
testSlideActive(1);
});
+ it('should buffer the slides if transition is clicked and only transition to the last requested', function() {
+ var carouselScope = elm.children().scope();
+
+ testSlideActive(0);
+ carouselScope.$currentTransition = null;
+ carouselScope.select(carouselScope.slides[1]);
+ $animate.flush();
+
+ testSlideActive(1);
+
+ carouselScope.$currentTransition = true;
+ carouselScope.select(carouselScope.slides[2]);
+ scope.$apply();
+
+ testSlideActive(1);
+
+ carouselScope.select(carouselScope.slides[0]);
+ scope.$apply();
+
+ testSlideActive(1);
+
+ carouselScope.$currentTransition = null;
+ $interval.flush(scope.interval);
+ $animate.flush();
+
+ testSlideActive(2);
+
+ $interval.flush(scope.interval);
+ $animate.flush();
+
+ testSlideActive(0);
+ });
+
it('issue 1414 - should not continue running timers after scope is destroyed', function() {
testSlideActive(0);
$interval.flush(scope.interval);
@@ -267,147 +385,212 @@ describe('carousel', function() {
expect($interval.cancel).toHaveBeenCalled();
});
- describe('slide order', function() {
-
- beforeEach(function() {
- scope.slides = [
- {active:false,content:'one', id:1},
- {active:false,content:'two', id:2},
- {active:false,content:'three', id:3}
- ];
- elm = $compile(
- '' +
- '' +
- '{{slide.content}}' +
- '' +
- ''
- )(scope);
- scope.$apply();
- scope.slides[0].id = 3;
- scope.slides[1].id = 1;
- scope.slides[2].id = 2;
- scope.$apply();
- });
+ it('issue 4390 - should reset the currentTransition if there are no slides', function() {
+ var carouselScope = elm.children().scope();
+ var next = elm.find('a.right');
+ scope.slides = [
+ {content:'new1', index: 1},
+ {content:'new2', index: 2},
+ {content:'new3', index: 3}
+ ];
+ scope.$apply();
- it('should change dom when an order of the slides was changed', function() {
- testSlideActive(0);
- var contents = elm.find('div.item');
- expect(contents.length).toBe(3);
- expect(contents.eq(0).text()).toBe('two');
- expect(contents.eq(1).text()).toBe('three');
- expect(contents.eq(2).text()).toBe('one');
- });
+ testSlideActive(0);
+ carouselScope.$currentTransition = true;
- it('should select next after order change', function() {
- testSlideActive(0);
- var next = elm.find('a.right');
- next.click();
- testSlideActive(1);
- });
+ scope.slides = [];
+ scope.$apply();
- it('should select prev after order change', function() {
- testSlideActive(0);
- var prev = elm.find('a.left');
- prev.click();
- testSlideActive(2);
- });
+ expect(carouselScope.$currentTransition).toBe(null);
+ });
+ });
- it('should add slide in the specified position', function() {
- testSlideActive(0);
- scope.slides[2].id = 4;
- scope.slides.push({active:false,content:'four', id:2});
- scope.$apply();
- var contents = elm.find('div.item');
- expect(contents.length).toBe(4);
- expect(contents.eq(0).text()).toBe('two');
- expect(contents.eq(1).text()).toBe('four');
- expect(contents.eq(2).text()).toBe('one');
- expect(contents.eq(3).text()).toBe('three');
- });
+ describe('slide order', function() {
+ var elm, scope;
+ beforeEach(function() {
+ scope = $rootScope.$new();
+ scope.slides = [
+ {content: 'one', id: 3},
+ {content: 'two', id: 1},
+ {content: 'three', id: 2}
+ ];
+ elm = $compile(
+ '
' +
+ '
' +
+ '{{slide.content}}' +
+ '
' +
+ '
'
+ )(scope);
+ scope.$apply();
+ });
- it('should remove slide after order change', function() {
- testSlideActive(0);
- scope.slides.splice(1, 1);
- scope.$apply();
- var contents = elm.find('div.item');
- expect(contents.length).toBe(2);
- expect(contents.eq(0).text()).toBe('three');
- expect(contents.eq(1).text()).toBe('one');
- });
+ function testSlideActive(slideIndex) {
+ for (var i = 0; i < scope.slides.length; i++) {
+ if (i === slideIndex) {
+ expect(scope.active).toBe(scope.slides[i].id);
+ } else {
+ expect(scope.active).not.toBe(scope.slides[i].id);
+ }
+ }
+ }
+
+ it('should change dom when the order of the slides changes', function() {
+ scope.slides[0].id = 3;
+ scope.slides[1].id = 2;
+ scope.slides[2].id = 1;
+ scope.$apply();
+ var contents = elm.find('div.item [ng-transclude]');
+ expect(contents.length).toBe(3);
+ expect(contents.eq(0).text()).toBe('three');
+ expect(contents.eq(1).text()).toBe('two');
+ expect(contents.eq(2).text()).toBe('one');
+ });
+ it('should select next after order change', function() {
+ testSlideActive(1);
+ var next = elm.find('a.right');
+ next.click();
+ testSlideActive(2);
+ });
+
+ it('should select prev after order change', function() {
+ testSlideActive(1);
+ var prev = elm.find('a.left');
+ prev.click();
+ testSlideActive(0);
});
+ it('should add slide in the specified position', function() {
+ testSlideActive(1);
+ scope.slides[2].id = 4;
+ scope.slides.push({content:'four', id: 5});
+ scope.$apply();
+ var contents = elm.find('div.item [ng-transclude]');
+ expect(contents.length).toBe(4);
+ expect(contents.eq(0).text()).toBe('two');
+ expect(contents.eq(1).text()).toBe('one');
+ expect(contents.eq(2).text()).toBe('three');
+ expect(contents.eq(3).text()).toBe('four');
+ });
+
+ it('should remove slide after order change', function() {
+ testSlideActive(1);
+ scope.slides.splice(1, 1);
+ scope.$apply();
+ var contents = elm.find('div.item [ng-transclude]');
+ expect(contents.length).toBe(2);
+ expect(contents.eq(0).text()).toBe('three');
+ expect(contents.eq(1).text()).toBe('one');
+ });
});
describe('controller', function() {
var scope, ctrl;
//create an array of slides and add to the scope
- var slides = [{'content':1},{'content':2},{'content':3},{'content':4}];
+ var slides = [
+ {'content': 1, index: 0},
+ {'content': 2, index: 1},
+ {'content': 3, index: 2},
+ {'content': 4, index: 3}
+ ];
beforeEach(function() {
scope = $rootScope.$new();
- ctrl = $controller('CarouselController', {$scope: scope, $element: null});
- for(var i = 0;i < slides.length;i++){
+ scope.noWrap = angular.noop;
+ ctrl = $controller('UibCarouselController', {$scope: scope, $element: angular.element('')});
+ 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 set first slide to active = true and the rest to false', function() {
+ angular.forEach(ctrl.slides, function(slide, i) {
+ if (i !== 0) {
+ expect(slide.slide.active).not.toBe(true);
+ } else {
+ expect(slide.slide.active).toBe(true);
+ }
});
+ });
- it('should add a new slide and not change the active slide', function() {
- var newSlide = {active: false};
- 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 add a new slide and not change the active slide', function() {
+ var newSlide = {active: false, index: 4};
+ expect(ctrl.slides.length).toBe(4);
+ ctrl.addSlide(newSlide);
+ expect(ctrl.slides.length).toBe(5);
+ expect(ctrl.slides[4].slide.active).toBe(false);
+ expect(ctrl.slides[0].slide.active).toBe(true);
+ });
- it('should remove slide and change active slide if needed', function() {
- expect(ctrl.slides.length).toBe(4);
- ctrl.removeSlide(ctrl.slides[0]);
- 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('should remove slide and change active slide if needed', function() {
+ expect(ctrl.slides.length).toBe(4);
+ ctrl.removeSlide(ctrl.slides[0].slide);
+ $timeout.flush(0);
+ expect(ctrl.slides.length).toBe(3);
+ expect(scope.active).toBe(1);
+ ctrl.select(ctrl.slides[2]);
+ ctrl.removeSlide(ctrl.slides[2].slide);
+ $timeout.flush(0);
+ expect(ctrl.slides.length).toBe(2);
+ expect(scope.active).toBe(2);
+ ctrl.removeSlide(ctrl.slides[0].slide);
+ $timeout.flush(0);
+ expect(ctrl.slides.length).toBe(1);
+ expect(scope.active).toBe(1);
+ });
- it('issue 1414 - should not continue running timers after scope is destroyed', function() {
- spyOn(scope, 'next').and.callThrough();
- scope.interval = 2000;
- scope.$digest();
+ it('issue 1414 - should not continue running timers after scope is destroyed', function() {
+ spyOn(scope, 'next');
+ scope.interval = 2000;
+ scope.$digest();
- $interval.flush(scope.interval);
- expect(scope.next.calls.count()).toBe(1);
+ $interval.flush(scope.interval);
+ expect(scope.next.calls.count()).toBe(1);
- scope.$destroy();
+ scope.$destroy();
- $interval.flush(scope.interval);
- expect(scope.next.calls.count()).toBe(1);
- });
+ $interval.flush(scope.interval);
+ expect(scope.next.calls.count()).toBe(1);
});
+
+ it('should be exposed in the template', inject(function($templateCache) {
+ $templateCache.put('uib/template/carousel/carousel.html', '
{{carousel.text}}
');
+
+ var scope = $rootScope.$new();
+ var elm = $compile('')(scope);
+ $rootScope.$digest();
+
+ var ctrl = elm.controller('uibCarousel');
+
+ expect(ctrl).toBeDefined();
+
+ ctrl.text = 'foo';
+ $rootScope.$digest();
+
+ expect(elm.html()).toBe('
foo
');
+ }));
+ });
+
+ it('should expose a custom model in the carousel slide', function() {
+ var scope = $rootScope.$new();
+ scope.slides = [
+ {active:false,content:'one'},
+ {active:false,content:'two'},
+ {active:false,content:'three'}
+ ];
+ var elm = $compile(
+ '
' +
+ '
' +
+ '{{slide.content}}' +
+ '
' +
+ '
'
+ )(scope);
+ $rootScope.$digest();
+
+ var ctrl = elm.controller('uibCarousel');
+
+ expect(angular.equals(ctrl.slides.map(function(slide) {
+ return slide.slide.actual;
+ }), scope.slides)).toBe(true);
});
});
diff --git a/src/collapse/collapse.js b/src/collapse/collapse.js
index 8f24d29ba7..7e605b641d 100644
--- a/src/collapse/collapse.js
+++ b/src/collapse/collapse.js
@@ -1,44 +1,127 @@
angular.module('ui.bootstrap.collapse', [])
- .directive('collapse', ['$animate', function ($animate) {
-
+ .directive('uibCollapse', ['$animate', '$q', '$parse', '$injector', function($animate, $q, $parse, $injector) {
+ var $animateCss = $injector.has('$animateCss') ? $injector.get('$animateCss') : null;
return {
- link: function (scope, element, attrs) {
+ link: function(scope, element, attrs) {
+ var expandingExpr = $parse(attrs.expanding),
+ expandedExpr = $parse(attrs.expanded),
+ collapsingExpr = $parse(attrs.collapsing),
+ collapsedExpr = $parse(attrs.collapsed),
+ horizontal = false,
+ css = {},
+ cssTo = {};
+
+ init();
+
+ function init() {
+ horizontal = !!('horizontal' in attrs);
+ if (horizontal) {
+ css = {
+ width: ''
+ };
+ cssTo = {width: '0'};
+ } else {
+ css = {
+ height: ''
+ };
+ cssTo = {height: '0'};
+ }
+ if (!scope.$eval(attrs.uibCollapse)) {
+ element.addClass('in')
+ .addClass('collapse')
+ .attr('aria-expanded', true)
+ .attr('aria-hidden', false)
+ .css(css);
+ }
+ }
+
+ function getScrollFromElement(element) {
+ if (horizontal) {
+ return {width: element.scrollWidth + 'px'};
+ }
+ return {height: element.scrollHeight + 'px'};
+ }
+
function expand() {
- element.removeClass('collapse').addClass('collapsing');
- $animate.addClass(element, 'in', {
- to: { height: element[0].scrollHeight + 'px' }
- }).then(expandDone);
+ if (element.hasClass('collapse') && element.hasClass('in')) {
+ return;
+ }
+
+ $q.resolve(expandingExpr(scope))
+ .then(function() {
+ element.removeClass('collapse')
+ .addClass('collapsing')
+ .attr('aria-expanded', true)
+ .attr('aria-hidden', false);
+
+ if ($animateCss) {
+ $animateCss(element, {
+ addClass: 'in',
+ easing: 'ease',
+ css: {
+ overflow: 'hidden'
+ },
+ to: getScrollFromElement(element[0])
+ }).start()['finally'](expandDone);
+ } else {
+ $animate.addClass(element, 'in', {
+ css: {
+ overflow: 'hidden'
+ },
+ to: getScrollFromElement(element[0])
+ }).then(expandDone);
+ }
+ }, angular.noop);
}
function expandDone() {
- element.removeClass('collapsing');
- element.css({height: 'auto'});
+ element.removeClass('collapsing')
+ .addClass('collapse')
+ .css(css);
+ expandedExpr(scope);
}
function collapse() {
- element
- // IMPORTANT: The height must be set before adding "collapsing" class.
- // Otherwise, the browser attempts to animate from height 0 (in
- // collapsing class) to the given height here.
- .css({height: element[0].scrollHeight + 'px'})
- // initially all panel collapse have the collapse class, this removal
- // prevents the animation from jumping to collapsed state
- .removeClass('collapse')
- .addClass('collapsing');
-
- $animate.removeClass(element, 'in', {
- to: {height: '0'}
- }).then(collapseDone);
+ if (!element.hasClass('collapse') && !element.hasClass('in')) {
+ return collapseDone();
+ }
+
+ $q.resolve(collapsingExpr(scope))
+ .then(function() {
+ element
+ // IMPORTANT: The width must be set before adding "collapsing" class.
+ // Otherwise, the browser attempts to animate from width 0 (in
+ // collapsing class) to the given width here.
+ .css(getScrollFromElement(element[0]))
+ // initially all panel collapse have the collapse class, this removal
+ // prevents the animation from jumping to collapsed state
+ .removeClass('collapse')
+ .addClass('collapsing')
+ .attr('aria-expanded', false)
+ .attr('aria-hidden', true);
+
+ if ($animateCss) {
+ $animateCss(element, {
+ removeClass: 'in',
+ to: cssTo
+ }).start()['finally'](collapseDone);
+ } else {
+ $animate.removeClass(element, 'in', {
+ to: cssTo
+ }).then(collapseDone);
+ }
+ }, angular.noop);
}
function collapseDone() {
- element.css({height: '0'}); // Required so that collapse works when animation is disabled
- element.removeClass('collapsing');
- element.addClass('collapse');
+ element.css(cssTo); // Required so that collapse works when animation is disabled
+ element.removeClass('collapsing')
+ .addClass('collapse');
+ collapsedExpr(scope);
}
- scope.$watch(attrs.collapse, function (shouldCollapse) {
+ scope.$watch(attrs.uibCollapse, function(shouldCollapse) {
if (shouldCollapse) {
collapse();
} else {
diff --git a/src/collapse/docs/demo.html b/src/collapse/docs/demo.html
index 131f1bf6a6..f5e06e7fbc 100644
--- a/src/collapse/docs/demo.html
+++ b/src/collapse/docs/demo.html
@@ -1,7 +1,40 @@
+
- Toggle collapse
+
Resize window to less than 768 pixels to display mobile menu toggle button.
+
-
-
Some content
+ Toggle collapse Vertically
+
+
+
Some content
+
+
+ Toggle collapse Horizontally
+
+
+
Some content
-
\ No newline at end of file
+
diff --git a/src/collapse/docs/demo.js b/src/collapse/docs/demo.js
index 897eecaf58..f685290001 100644
--- a/src/collapse/docs/demo.js
+++ b/src/collapse/docs/demo.js
@@ -1,3 +1,5 @@
angular.module('ui.bootstrap.demo').controller('CollapseDemoCtrl', function ($scope) {
+ $scope.isNavCollapsed = true;
$scope.isCollapsed = false;
+ $scope.isCollapsedHorizontal = false;
});
diff --git a/src/collapse/docs/readme.md b/src/collapse/docs/readme.md
index 7699f3775a..5cdacfd3d0 100644
--- a/src/collapse/docs/readme.md
+++ b/src/collapse/docs/readme.md
@@ -1,2 +1,37 @@
-AngularJS version of Bootstrap's collapse plugin.
-Provides a simple way to hide and show an element with a css transition
+**uib-collapse** provides a simple way to hide and show an element with a css transition
+
+### uib-collapse settings
+
+* `collapsed()`
+ $ -
+ An optional expression called after the element finished collapsing.
+
+* `collapsing()`
+ $ -
+ An optional expression called before the element begins collapsing.
+ If the expression returns a promise, animation won't start until the promise resolves.
+ If the returned promise is rejected, collapsing will be cancelled.
+
+* `expanded()`
+ $ -
+ An optional expression called after the element finished expanding.
+
+* `expanding()`
+ $ -
+ An optional expression called before the element begins expanding.
+ If the expression returns a promise, animation won't start until the promise resolves.
+ If the returned promise is rejected, expanding will be cancelled.
+
+* `uib-collapse`
+ $
+
+ _(Default: `false`)_ -
+ Whether the element should be collapsed or not.
+
+* `horizontal`
+ $ -
+ An optional attribute that permit to collapse horizontally.
+
+### Known Issues
+
+When using the `horizontal` attribute with this directive, CSS can reflow as the collapse element goes from `0px` to its desired end width, which can result in height changes. This can cause animations to not appear to run. The best way around this is to set a fixed height via CSS on the horizontal collapse element so that this situation does not occur, and so the animation can run as expected.
diff --git a/src/collapse/index.js b/src/collapse/index.js
new file mode 100644
index 0000000000..b720e842c4
--- /dev/null
+++ b/src/collapse/index.js
@@ -0,0 +1,7 @@
+require('./collapse');
+
+var MODULE_NAME = 'ui.bootstrap.module.collapse';
+
+angular.module(MODULE_NAME, ['ui.bootstrap.collapse']);
+
+module.exports = MODULE_NAME;
diff --git a/src/collapse/test/collapse.spec.js b/src/collapse/test/collapse.spec.js
index 8ed51fb6ec..f9b8c4e771 100644
--- a/src/collapse/test/collapse.spec.js
+++ b/src/collapse/test/collapse.spec.js
@@ -1,18 +1,24 @@
-describe('collapse directive', function () {
-
- var scope, $compile, $animate;
- var element;
+describe('collapse directive', function() {
+ var element, compileFn, scope, $compile, $animate, $q;
beforeEach(module('ui.bootstrap.collapse'));
beforeEach(module('ngAnimateMock'));
- beforeEach(inject(function(_$rootScope_, _$compile_, _$animate_) {
+ beforeEach(inject(function(_$rootScope_, _$compile_, _$animate_, _$q_) {
scope = _$rootScope_;
$compile = _$compile_;
$animate = _$animate_;
+ $q = _$q_;
}));
beforeEach(function() {
- element = $compile('
Some Content
')(scope);
+ element = angular.element(
+ '
'
+ + 'Some Content
');
+ compileFn = $compile(element);
angular.element(document.body).append(element);
});
@@ -20,62 +26,146 @@ describe('collapse directive', function () {
element.remove();
});
- it('should be hidden on initialization if isCollapsed = true without transition', function() {
+ function initCallbacks() {
+ scope.collapsing = jasmine.createSpy('scope.collapsing');
+ scope.collapsed = jasmine.createSpy('scope.collapsed');
+ scope.expanding = jasmine.createSpy('scope.expanding');
+ scope.expanded = jasmine.createSpy('scope.expanded');
+ }
+
+ function assertCallbacks(expected) {
+ ['collapsing', 'collapsed', 'expanding', 'expanded'].forEach(function(cbName) {
+ if (expected[cbName]) {
+ expect(scope[cbName]).toHaveBeenCalled();
+ } else {
+ expect(scope[cbName]).not.toHaveBeenCalled();
+ }
+ });
+ }
+
+ it('should be hidden on initialization if isCollapsed = true', function() {
+ initCallbacks();
scope.isCollapsed = true;
+ compileFn(scope);
scope.$digest();
- $animate.triggerCallbacks();
- //No animation timeout here
expect(element.height()).toBe(0);
+ assertCallbacks({ collapsed: true });
+ });
+
+ it('should not trigger any animation on initialization if isCollapsed = true', function() {
+ var wrapperFn = function() {
+ $animate.flush();
+ };
+
+ scope.isCollapsed = true;
+ compileFn(scope);
+ scope.$digest();
+
+ expect(wrapperFn).toThrowError(/No pending animations ready to be closed or flushed/);
});
- it('should collapse if isCollapsed = true with animation on subsequent use', function() {
+ it('should collapse if isCollapsed = true on subsequent use', function() {
scope.isCollapsed = false;
+ compileFn(scope);
scope.$digest();
+ initCallbacks();
scope.isCollapsed = true;
scope.$digest();
- $animate.triggerCallbacks();
+ $animate.flush();
expect(element.height()).toBe(0);
+ assertCallbacks({ collapsing: true, collapsed: true });
});
- it('should be shown on initialization if isCollapsed = false without transition', function() {
+ it('should show after toggled from collapsed', function() {
+ initCallbacks();
+ scope.isCollapsed = true;
+ compileFn(scope);
+ scope.$digest();
+ expect(element.height()).toBe(0);
+ assertCallbacks({ collapsed: true });
+ scope.collapsed.calls.reset();
+
scope.isCollapsed = false;
scope.$digest();
- //No animation timeout here
+ $animate.flush();
expect(element.height()).not.toBe(0);
+ assertCallbacks({ expanding: true, expanded: true });
+ });
+
+ it('should not trigger any animation on initialization if isCollapsed = false', function() {
+ var wrapperFn = function() {
+ $animate.flush();
+ };
+
+ scope.isCollapsed = false;
+ compileFn(scope);
+ scope.$digest();
+
+ expect(wrapperFn).toThrowError(/No pending animations ready to be closed or flushed/);
});
- it('should expand if isCollapsed = false with animation on subsequent use', function() {
+ it('should expand if isCollapsed = false on subsequent use', function() {
scope.isCollapsed = false;
+ compileFn(scope);
scope.$digest();
scope.isCollapsed = true;
scope.$digest();
+ $animate.flush();
+ initCallbacks();
scope.isCollapsed = false;
scope.$digest();
- $animate.triggerCallbacks();
+ $animate.flush();
expect(element.height()).not.toBe(0);
+ assertCallbacks({ expanding: true, expanded: true });
});
- it('should expand if isCollapsed = true with animation on subsequent uses', function() {
+ it('should collapse if isCollapsed = true on subsequent uses', function() {
scope.isCollapsed = false;
+ compileFn(scope);
scope.$digest();
scope.isCollapsed = true;
scope.$digest();
+ $animate.flush();
scope.isCollapsed = false;
scope.$digest();
+ $animate.flush();
+ initCallbacks();
scope.isCollapsed = true;
scope.$digest();
- $animate.triggerCallbacks();
- expect(element.height()).toBe(0);
- $animate.triggerCallbacks();
+ $animate.flush();
expect(element.height()).toBe(0);
+ assertCallbacks({ collapsing: true, collapsed: true });
});
- describe('dynamic content', function() {
+ it('should change aria-expanded attribute', function() {
+ scope.isCollapsed = false;
+ compileFn(scope);
+ scope.$digest();
+ expect(element.attr('aria-expanded')).toBe('true');
+
+ scope.isCollapsed = true;
+ scope.$digest();
+ $animate.flush();
+ expect(element.attr('aria-expanded')).toBe('false');
+ });
+ it('should change aria-hidden attribute', function() {
+ scope.isCollapsed = false;
+ compileFn(scope);
+ scope.$digest();
+ expect(element.attr('aria-hidden')).toBe('false');
+
+ scope.isCollapsed = true;
+ scope.$digest();
+ $animate.flush();
+ expect(element.attr('aria-hidden')).toBe('true');
+ });
+
+ describe('dynamic content', function() {
var element;
beforeEach(function() {
- element = angular.element('
Initial content
Additional content
');
+ element = angular.element('
Initial content
Additional content
');
$compile(element)(scope);
angular.element(document.body).append(element);
});
@@ -88,7 +178,6 @@ describe('collapse directive', function () {
scope.exp = false;
scope.isCollapsed = false;
scope.$digest();
- $animate.triggerCallbacks();
var collapseHeight = element.height();
scope.exp = true;
scope.$digest();
@@ -99,12 +188,101 @@ describe('collapse directive', function () {
scope.exp = true;
scope.isCollapsed = false;
scope.$digest();
- $animate.triggerCallbacks();
var collapseHeight = element.height();
scope.exp = false;
scope.$digest();
expect(element.height()).toBeLessThan(collapseHeight);
});
+ });
+
+ describe('expanding callback returning a promise', function() {
+ var defer, collapsedHeight;
+
+ beforeEach(function() {
+ defer = $q.defer();
+
+ scope.isCollapsed = true;
+ scope.expanding = function() {
+ return defer.promise;
+ };
+ compileFn(scope);
+ scope.$digest();
+ collapsedHeight = element.height();
+
+ // set flag to expand ...
+ scope.isCollapsed = false;
+ scope.$digest();
+
+ // ... shouldn't expand yet ...
+ expect(element.attr('aria-expanded')).not.toBe('true');
+ expect(element.height()).toBe(collapsedHeight);
+ });
+
+ it('should wait for it to resolve before animating', function() {
+ defer.resolve();
+
+ // should now expand
+ scope.$digest();
+ $animate.flush();
+
+ expect(element.attr('aria-expanded')).toBe('true');
+ expect(element.height()).toBeGreaterThan(collapsedHeight);
+ });
+
+ it('should not animate if it rejects', function() {
+ defer.reject();
+
+ // should NOT expand
+ scope.$digest();
+ expect(element.attr('aria-expanded')).not.toBe('true');
+ expect(element.height()).toBe(collapsedHeight);
+ });
});
+
+ describe('collapsing callback returning a promise', function() {
+ var defer, expandedHeight;
+
+ beforeEach(function() {
+ defer = $q.defer();
+ scope.isCollapsed = false;
+ scope.collapsing = function() {
+ return defer.promise;
+ };
+ compileFn(scope);
+ scope.$digest();
+
+ expandedHeight = element.height();
+
+ // set flag to collapse ...
+ scope.isCollapsed = true;
+ scope.$digest();
+
+ // ... but it shouldn't collapse yet ...
+ expect(element.attr('aria-expanded')).not.toBe('false');
+ expect(element.height()).toBe(expandedHeight);
+ });
+
+ it('should wait for it to resolve before animating', function() {
+ defer.resolve();
+
+ // should now collapse
+ scope.$digest();
+ $animate.flush();
+
+ expect(element.attr('aria-expanded')).toBe('false');
+ expect(element.height()).toBeLessThan(expandedHeight);
+ });
+
+ it('should not animate if it rejects', function() {
+ defer.reject();
+
+ // should NOT collapse
+ scope.$digest();
+
+ expect(element.attr('aria-expanded')).not.toBe('false');
+ expect(element.height()).toBe(expandedHeight);
+ });
+ });
+
});
diff --git a/src/collapse/test/collapseHorizontally.spec.js b/src/collapse/test/collapseHorizontally.spec.js
new file mode 100644
index 0000000000..e5b22bdb56
--- /dev/null
+++ b/src/collapse/test/collapseHorizontally.spec.js
@@ -0,0 +1,255 @@
+describe('collapse directive', function() {
+ var elementH, compileFnH, scope, $compile, $animate, $q;
+
+ beforeEach(module('ui.bootstrap.collapse'));
+ beforeEach(module('ngAnimateMock'));
+ beforeEach(inject(function(_$rootScope_, _$compile_, _$animate_, _$q_) {
+ scope = _$rootScope_;
+ $compile = _$compile_;
+ $animate = _$animate_;
+ $q = _$q_;
+ }));
+
+ beforeEach(function() {
+ elementH = angular.element(
+ '
'
+ + 'Some Content
');
+ compileFnH = $compile(elementH);
+ angular.element(document.body).append(elementH);
+ });
+
+ afterEach(function() {
+ elementH.remove();
+ });
+
+ function initCallbacks() {
+ scope.collapsing = jasmine.createSpy('scope.collapsing');
+ scope.collapsed = jasmine.createSpy('scope.collapsed');
+ scope.expanding = jasmine.createSpy('scope.expanding');
+ scope.expanded = jasmine.createSpy('scope.expanded');
+ }
+
+ function assertCallbacks(expected) {
+ ['collapsing', 'collapsed', 'expanding', 'expanded'].forEach(function(cbName) {
+ if (expected[cbName]) {
+ expect(scope[cbName]).toHaveBeenCalled();
+ } else {
+ expect(scope[cbName]).not.toHaveBeenCalled();
+ }
+ });
+ }
+
+ it('should be hidden on initialization if isCollapsed = true', function() {
+ initCallbacks();
+ scope.isCollapsed = true;
+ compileFnH(scope);
+ scope.$digest();
+ expect(elementH.width()).toBe(0);
+ assertCallbacks({ collapsed: true });
+ });
+
+ it('should not trigger any animation on initialization if isCollapsed = true', function() {
+ var wrapperFn = function() {
+ $animate.flush();
+ };
+
+ scope.isCollapsed = true;
+ compileFnH(scope);
+ scope.$digest();
+
+ expect(wrapperFn).toThrowError(/No pending animations ready to be closed or flushed/);
+ });
+
+ it('should collapse if isCollapsed = true on subsequent use', function() {
+ scope.isCollapsed = false;
+ compileFnH(scope);
+ scope.$digest();
+ initCallbacks();
+ scope.isCollapsed = true;
+ scope.$digest();
+ $animate.flush();
+ expect(elementH.width()).toBe(0);
+ assertCallbacks({ collapsing: true, collapsed: true });
+ });
+
+ it('should show after toggled from collapsed', function() {
+ initCallbacks();
+ scope.isCollapsed = true;
+ compileFnH(scope);
+ scope.$digest();
+ expect(elementH.width()).toBe(0);
+ assertCallbacks({ collapsed: true });
+ scope.collapsed.calls.reset();
+
+ scope.isCollapsed = false;
+ scope.$digest();
+ $animate.flush();
+ expect(elementH.width()).not.toBe(0);
+ assertCallbacks({ expanding: true, expanded: true });
+ });
+
+ it('should not trigger any animation on initialization if isCollapsed = false', function() {
+ var wrapperFn = function() {
+ $animate.flush();
+ };
+
+ scope.isCollapsed = false;
+ compileFnH(scope);
+ scope.$digest();
+
+ expect(wrapperFn).toThrowError(/No pending animations ready to be closed or flushed/);
+ });
+
+ it('should expand if isCollapsed = false on subsequent use', function() {
+ scope.isCollapsed = false;
+ compileFnH(scope);
+ scope.$digest();
+ scope.isCollapsed = true;
+ scope.$digest();
+ $animate.flush();
+ initCallbacks();
+ scope.isCollapsed = false;
+ scope.$digest();
+ $animate.flush();
+ expect(elementH.width()).not.toBe(0);
+ assertCallbacks({ expanding: true, expanded: true });
+ });
+
+ it('should collapse if isCollapsed = true on subsequent uses', function() {
+ scope.isCollapsed = false;
+ compileFnH(scope);
+ scope.$digest();
+ scope.isCollapsed = true;
+ scope.$digest();
+ $animate.flush();
+ scope.isCollapsed = false;
+ scope.$digest();
+ $animate.flush();
+ initCallbacks();
+ scope.isCollapsed = true;
+ scope.$digest();
+ $animate.flush();
+ expect(elementH.width()).toBe(0);
+ assertCallbacks({ collapsing: true, collapsed: true });
+ });
+
+ it('should change aria-expanded attribute', function() {
+ scope.isCollapsed = false;
+ compileFnH(scope);
+ scope.$digest();
+ expect(elementH.attr('aria-expanded')).toBe('true');
+
+ scope.isCollapsed = true;
+ scope.$digest();
+ $animate.flush();
+ expect(elementH.attr('aria-expanded')).toBe('false');
+ });
+
+ it('should change aria-hidden attribute', function() {
+ scope.isCollapsed = false;
+ compileFnH(scope);
+ scope.$digest();
+ expect(elementH.attr('aria-hidden')).toBe('false');
+
+ scope.isCollapsed = true;
+ scope.$digest();
+ $animate.flush();
+ expect(elementH.attr('aria-hidden')).toBe('true');
+ });
+
+ describe('expanding callback returning a promise', function() {
+ var defer, collapsedWidth;
+
+ beforeEach(function() {
+ defer = $q.defer();
+
+ scope.isCollapsed = true;
+ scope.expanding = function() {
+ return defer.promise;
+ };
+ compileFnH(scope);
+ scope.$digest();
+ collapsedWidth = elementH.width();
+
+ // set flag to expand ...
+ scope.isCollapsed = false;
+ scope.$digest();
+
+ // ... shouldn't expand yet ...
+ expect(elementH.attr('aria-expanded')).not.toBe('true');
+ expect(elementH.width()).toBe(collapsedWidth);
+ });
+
+ it('should wait for it to resolve before animating', function() {
+ defer.resolve();
+
+ // should now expand
+ scope.$digest();
+ $animate.flush();
+
+ expect(elementH.attr('aria-expanded')).toBe('true');
+ expect(elementH.width()).toBeGreaterThan(collapsedWidth);
+ });
+
+ it('should not animate if it rejects', function() {
+ defer.reject();
+
+ // should NOT expand
+ scope.$digest();
+
+ expect(elementH.attr('aria-expanded')).not.toBe('true');
+ expect(elementH.width()).toBe(collapsedWidth);
+ });
+ });
+
+ describe('collapsing callback returning a promise', function() {
+ var defer, expandedWidth;
+
+ beforeEach(function() {
+ defer = $q.defer();
+ scope.isCollapsed = false;
+ scope.collapsing = function() {
+ return defer.promise;
+ };
+ compileFnH(scope);
+ scope.$digest();
+
+ expandedWidth = elementH.width();
+
+ // set flag to collapse ...
+ scope.isCollapsed = true;
+ scope.$digest();
+
+ // ... but it shouldn't collapse yet ...
+ expect(elementH.attr('aria-expanded')).not.toBe('false');
+ expect(elementH.width()).toBe(expandedWidth);
+ });
+
+ it('should wait for it to resolve before animating', function() {
+ defer.resolve();
+
+ // should now collapse
+ scope.$digest();
+ $animate.flush();
+
+ expect(elementH.attr('aria-expanded')).toBe('false');
+ expect(elementH.width()).toBeLessThan(expandedWidth);
+ });
+
+ it('should not animate if it rejects', function() {
+ defer.reject();
+
+ // should NOT collapse
+ scope.$digest();
+
+ expect(elementH.attr('aria-expanded')).not.toBe('false');
+ expect(elementH.width()).toBe(expandedWidth);
+ });
+ });
+
+});
diff --git a/src/dateparser/dateparser.js b/src/dateparser/dateparser.js
index eb323623b6..d770dff31b 100644
--- a/src/dateparser/dateparser.js
+++ b/src/dateparser/dateparser.js
@@ -1,102 +1,317 @@
angular.module('ui.bootstrap.dateparser', [])
-.service('dateParser', ['$locale', 'orderByFilter', function($locale, orderByFilter) {
+.service('uibDateParser', ['$log', '$locale', 'dateFilter', 'orderByFilter', 'filterFilter', function($log, $locale, dateFilter, orderByFilter, filterFilter) {
// Pulled from https://github.com/mbostock/d3/blob/master/src/format/requote.js
var SPECIAL_CHARACTERS_REGEXP = /[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g;
- 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('|')
- },
- 'HH': {
- regex: '(?:0|1)[0-9]|2[0-3]',
- apply: function(value) { this.hours = +value; }
- },
- 'H': {
- regex: '1?[0-9]|2[0-3]',
- apply: function(value) { this.hours = +value; }
- },
- 'mm': {
- regex: '[0-5][0-9]',
- apply: function(value) { this.minutes = +value; }
- },
- 'm': {
- regex: '[0-9]|[1-5][0-9]',
- apply: function(value) { this.minutes = +value; }
- },
- 'sss': {
- regex: '[0-9][0-9][0-9]',
- apply: function(value) { this.milliseconds = +value; }
- },
- 'ss': {
- regex: '[0-5][0-9]',
- apply: function(value) { this.seconds = +value; }
- },
- 's': {
- regex: '[0-9]|[1-5][0-9]',
- apply: function(value) { this.seconds = +value; }
+ var localeId;
+ var formatCodeToRegex;
+
+ this.init = function() {
+ localeId = $locale.id;
+
+ this.parsers = {};
+ this.formatters = {};
+
+ formatCodeToRegex = [
+ {
+ key: 'yyyy',
+ regex: '\\d{4}',
+ apply: function(value) { this.year = +value; },
+ formatter: function(date) {
+ var _date = new Date();
+ _date.setFullYear(Math.abs(date.getFullYear()));
+ return dateFilter(_date, 'yyyy');
+ }
+ },
+ {
+ key: 'yy',
+ regex: '\\d{2}',
+ apply: function(value) { value = +value; this.year = value < 69 ? value + 2000 : value + 1900; },
+ formatter: function(date) {
+ var _date = new Date();
+ _date.setFullYear(Math.abs(date.getFullYear()));
+ return dateFilter(_date, 'yy');
+ }
+ },
+ {
+ key: 'y',
+ regex: '\\d{1,4}',
+ apply: function(value) { this.year = +value; },
+ formatter: function(date) {
+ var _date = new Date();
+ _date.setFullYear(Math.abs(date.getFullYear()));
+ return dateFilter(_date, 'y');
+ }
+ },
+ {
+ key: 'M!',
+ regex: '0?[1-9]|1[0-2]',
+ apply: function(value) { this.month = value - 1; },
+ formatter: function(date) {
+ var value = date.getMonth();
+ if (/^[0-9]$/.test(value)) {
+ return dateFilter(date, 'MM');
+ }
+
+ return dateFilter(date, 'M');
+ }
+ },
+ {
+ key: 'MMMM',
+ regex: $locale.DATETIME_FORMATS.MONTH.join('|'),
+ apply: function(value) { this.month = $locale.DATETIME_FORMATS.MONTH.indexOf(value); },
+ formatter: function(date) { return dateFilter(date, 'MMMM'); }
+ },
+ {
+ key: 'MMM',
+ regex: $locale.DATETIME_FORMATS.SHORTMONTH.join('|'),
+ apply: function(value) { this.month = $locale.DATETIME_FORMATS.SHORTMONTH.indexOf(value); },
+ formatter: function(date) { return dateFilter(date, 'MMM'); }
+ },
+ {
+ key: 'MM',
+ regex: '0[1-9]|1[0-2]',
+ apply: function(value) { this.month = value - 1; },
+ formatter: function(date) { return dateFilter(date, 'MM'); }
+ },
+ {
+ key: 'M',
+ regex: '[1-9]|1[0-2]',
+ apply: function(value) { this.month = value - 1; },
+ formatter: function(date) { return dateFilter(date, 'M'); }
+ },
+ {
+ key: 'd!',
+ regex: '[0-2]?[0-9]{1}|3[0-1]{1}',
+ apply: function(value) { this.date = +value; },
+ formatter: function(date) {
+ var value = date.getDate();
+ if (/^[1-9]$/.test(value)) {
+ return dateFilter(date, 'dd');
+ }
+
+ return dateFilter(date, 'd');
+ }
+ },
+ {
+ key: 'dd',
+ regex: '[0-2][0-9]{1}|3[0-1]{1}',
+ apply: function(value) { this.date = +value; },
+ formatter: function(date) { return dateFilter(date, 'dd'); }
+ },
+ {
+ key: 'd',
+ regex: '[1-2]?[0-9]{1}|3[0-1]{1}',
+ apply: function(value) { this.date = +value; },
+ formatter: function(date) { return dateFilter(date, 'd'); }
+ },
+ {
+ key: 'EEEE',
+ regex: $locale.DATETIME_FORMATS.DAY.join('|'),
+ formatter: function(date) { return dateFilter(date, 'EEEE'); }
+ },
+ {
+ key: 'EEE',
+ regex: $locale.DATETIME_FORMATS.SHORTDAY.join('|'),
+ formatter: function(date) { return dateFilter(date, 'EEE'); }
+ },
+ {
+ key: 'HH',
+ regex: '(?:0|1)[0-9]|2[0-3]',
+ apply: function(value) { this.hours = +value; },
+ formatter: function(date) { return dateFilter(date, 'HH'); }
+ },
+ {
+ key: 'hh',
+ regex: '0[0-9]|1[0-2]',
+ apply: function(value) { this.hours = +value; },
+ formatter: function(date) { return dateFilter(date, 'hh'); }
+ },
+ {
+ key: 'H',
+ regex: '1?[0-9]|2[0-3]',
+ apply: function(value) { this.hours = +value; },
+ formatter: function(date) { return dateFilter(date, 'H'); }
+ },
+ {
+ key: 'h',
+ regex: '[0-9]|1[0-2]',
+ apply: function(value) { this.hours = +value; },
+ formatter: function(date) { return dateFilter(date, 'h'); }
+ },
+ {
+ key: 'mm',
+ regex: '[0-5][0-9]',
+ apply: function(value) { this.minutes = +value; },
+ formatter: function(date) { return dateFilter(date, 'mm'); }
+ },
+ {
+ key: 'm',
+ regex: '[0-9]|[1-5][0-9]',
+ apply: function(value) { this.minutes = +value; },
+ formatter: function(date) { return dateFilter(date, 'm'); }
+ },
+ {
+ key: 'sss',
+ regex: '[0-9][0-9][0-9]',
+ apply: function(value) { this.milliseconds = +value; },
+ formatter: function(date) { return dateFilter(date, 'sss'); }
+ },
+ {
+ key: 'ss',
+ regex: '[0-5][0-9]',
+ apply: function(value) { this.seconds = +value; },
+ formatter: function(date) { return dateFilter(date, 'ss'); }
+ },
+ {
+ key: 's',
+ regex: '[0-9]|[1-5][0-9]',
+ apply: function(value) { this.seconds = +value; },
+ formatter: function(date) { return dateFilter(date, 's'); }
+ },
+ {
+ key: 'a',
+ regex: $locale.DATETIME_FORMATS.AMPMS.join('|'),
+ apply: function(value) {
+ if (this.hours === 12) {
+ this.hours = 0;
+ }
+
+ if (value === 'PM') {
+ this.hours += 12;
+ }
+ },
+ formatter: function(date) { return dateFilter(date, 'a'); }
+ },
+ {
+ key: 'Z',
+ regex: '[+-]\\d{4}',
+ apply: function(value) {
+ var matches = value.match(/([+-])(\d{2})(\d{2})/),
+ sign = matches[1],
+ hours = matches[2],
+ minutes = matches[3];
+ this.hours += toInt(sign + hours);
+ this.minutes += toInt(sign + minutes);
+ },
+ formatter: function(date) {
+ return dateFilter(date, 'Z');
+ }
+ },
+ {
+ key: 'ww',
+ regex: '[0-4][0-9]|5[0-3]',
+ formatter: function(date) { return dateFilter(date, 'ww'); }
+ },
+ {
+ key: 'w',
+ regex: '[0-9]|[1-4][0-9]|5[0-3]',
+ formatter: function(date) { return dateFilter(date, 'w'); }
+ },
+ {
+ key: 'GGGG',
+ regex: $locale.DATETIME_FORMATS.ERANAMES.join('|').replace(/\s/g, '\\s'),
+ formatter: function(date) { return dateFilter(date, 'GGGG'); }
+ },
+ {
+ key: 'GGG',
+ regex: $locale.DATETIME_FORMATS.ERAS.join('|'),
+ formatter: function(date) { return dateFilter(date, 'GGG'); }
+ },
+ {
+ key: 'GG',
+ regex: $locale.DATETIME_FORMATS.ERAS.join('|'),
+ formatter: function(date) { return dateFilter(date, 'GG'); }
+ },
+ {
+ key: 'G',
+ regex: $locale.DATETIME_FORMATS.ERAS.join('|'),
+ formatter: function(date) { return dateFilter(date, 'G'); }
+ }
+ ];
+
+ if (angular.version.major >= 1 && angular.version.minor > 4) {
+ formatCodeToRegex.push({
+ key: 'LLLL',
+ regex: $locale.DATETIME_FORMATS.STANDALONEMONTH.join('|'),
+ apply: function(value) { this.month = $locale.DATETIME_FORMATS.STANDALONEMONTH.indexOf(value); },
+ formatter: function(date) { return dateFilter(date, 'LLLL'); }
+ });
}
};
+ this.init();
+
+ function getFormatCodeToRegex(key) {
+ return filterFilter(formatCodeToRegex, {key: key}, true)[0];
+ }
+
+ this.getParser = function (key) {
+ var f = getFormatCodeToRegex(key);
+ return f && f.apply || null;
+ };
+
+ this.overrideParser = function (key, parser) {
+ var f = getFormatCodeToRegex(key);
+ if (f && angular.isFunction(parser)) {
+ this.parsers = {};
+ f.apply = parser;
+ }
+ }.bind(this);
+
function createParser(format) {
var map = [], regex = format.split('');
- angular.forEach(formatCodeToRegex, function(data, code) {
- var index = format.indexOf(code);
+ // check for literal values
+ var quoteIndex = format.indexOf('\'');
+ if (quoteIndex > -1) {
+ var inLiteral = false;
+ format = format.split('');
+ for (var i = quoteIndex; i < format.length; i++) {
+ if (inLiteral) {
+ if (format[i] === '\'') {
+ if (i + 1 < format.length && format[i+1] === '\'') { // escaped single quote
+ format[i+1] = '$';
+ regex[i+1] = '';
+ } else { // end of literal
+ regex[i] = '';
+ inLiteral = false;
+ }
+ }
+ format[i] = '$';
+ } else {
+ if (format[i] === '\'') { // start of literal
+ format[i] = '$';
+ regex[i] = '';
+ inLiteral = true;
+ }
+ }
+ }
+
+ format = format.join('');
+ }
+
+ angular.forEach(formatCodeToRegex, function(data) {
+ var index = format.indexOf(data.key);
if (index > -1) {
format = format.split('');
regex[index] = '(' + data.regex + ')';
format[index] = '$'; // Custom symbol to define consumed part of format
- for (var i = index + 1, n = index + code.length; i < n; i++) {
+ for (var i = index + 1, n = index + data.key.length; i < n; i++) {
regex[i] = '';
format[i] = '$';
}
format = format.join('');
- map.push({ index: index, apply: data.apply });
+ map.push({
+ index: index,
+ key: data.key,
+ apply: data.apply,
+ matcher: data.regex
+ });
}
});
@@ -106,26 +321,116 @@ angular.module('ui.bootstrap.dateparser', [])
};
}
+ function createFormatter(format) {
+ var formatters = [];
+ var i = 0;
+ var formatter, literalIdx;
+ while (i < format.length) {
+ if (angular.isNumber(literalIdx)) {
+ if (format.charAt(i) === '\'') {
+ if (i + 1 >= format.length || format.charAt(i + 1) !== '\'') {
+ formatters.push(constructLiteralFormatter(format, literalIdx, i));
+ literalIdx = null;
+ }
+ } else if (i === format.length) {
+ while (literalIdx < format.length) {
+ formatter = constructFormatterFromIdx(format, literalIdx);
+ formatters.push(formatter);
+ literalIdx = formatter.endIdx;
+ }
+ }
+
+ i++;
+ continue;
+ }
+
+ if (format.charAt(i) === '\'') {
+ literalIdx = i;
+ i++;
+ continue;
+ }
+
+ formatter = constructFormatterFromIdx(format, i);
+
+ formatters.push(formatter.parser);
+ i = formatter.endIdx;
+ }
+
+ return formatters;
+ }
+
+ function constructLiteralFormatter(format, literalIdx, endIdx) {
+ return function() {
+ return format.substr(literalIdx + 1, endIdx - literalIdx - 1);
+ };
+ }
+
+ function constructFormatterFromIdx(format, i) {
+ var currentPosStr = format.substr(i);
+ for (var j = 0; j < formatCodeToRegex.length; j++) {
+ if (new RegExp('^' + formatCodeToRegex[j].key).test(currentPosStr)) {
+ var data = formatCodeToRegex[j];
+ return {
+ endIdx: i + data.key.length,
+ parser: data.formatter
+ };
+ }
+ }
+
+ return {
+ endIdx: i + 1,
+ parser: function() {
+ return currentPosStr.charAt(0);
+ }
+ };
+ }
+
+ this.filter = function(date, format) {
+ if (!angular.isDate(date) || isNaN(date) || !format) {
+ return '';
+ }
+
+ format = $locale.DATETIME_FORMATS[format] || format;
+
+ if ($locale.id !== localeId) {
+ this.init();
+ }
+
+ if (!this.formatters[format]) {
+ this.formatters[format] = createFormatter(format);
+ }
+
+ var formatters = this.formatters[format];
+
+ return formatters.reduce(function(str, formatter) {
+ return str + formatter(date);
+ }, '');
+ };
+
this.parse = function(input, format, baseDate) {
- if ( !angular.isString(input) || !format ) {
+ if (!angular.isString(input) || !format) {
return input;
}
format = $locale.DATETIME_FORMATS[format] || format;
format = format.replace(SPECIAL_CHARACTERS_REGEXP, '\\$&');
- if ( !this.parsers[format] ) {
- this.parsers[format] = createParser(format);
+ if ($locale.id !== localeId) {
+ this.init();
+ }
+
+ if (!this.parsers[format]) {
+ this.parsers[format] = createParser(format, 'apply');
}
var parser = this.parsers[format],
regex = parser.regex,
map = parser.map,
- results = input.match(regex);
-
- if ( results && results.length ) {
+ results = input.match(regex),
+ tzOffset = false;
+ if (results && results.length) {
var fields, dt;
- if (baseDate) {
+ if (angular.isDate(baseDate) && !isNaN(baseDate.getTime())) {
fields = {
year: baseDate.getFullYear(),
month: baseDate.getMonth(),
@@ -136,19 +441,40 @@ angular.module('ui.bootstrap.dateparser', [])
milliseconds: baseDate.getMilliseconds()
};
} else {
+ if (baseDate) {
+ $log.warn('dateparser:', 'baseDate is not a valid date');
+ }
fields = { year: 1900, month: 0, date: 1, hours: 0, minutes: 0, seconds: 0, milliseconds: 0 };
}
- for( var i = 1, n = results.length; i < n; i++ ) {
- var mapper = map[i-1];
- if ( mapper.apply ) {
+ for (var i = 1, n = results.length; i < n; i++) {
+ var mapper = map[i - 1];
+ if (mapper.matcher === 'Z') {
+ tzOffset = true;
+ }
+
+ if (mapper.apply) {
mapper.apply.call(fields, results[i]);
}
}
- if ( isValid(fields.year, fields.month, fields.date) ) {
- dt = new Date(fields.year, fields.month, fields.date, fields.hours, fields.minutes, fields.seconds,
- fields.milliseconds || 0);
+ var datesetter = tzOffset ? Date.prototype.setUTCFullYear :
+ Date.prototype.setFullYear;
+ var timesetter = tzOffset ? Date.prototype.setUTCHours :
+ Date.prototype.setHours;
+
+ if (isValid(fields.year, fields.month, fields.date)) {
+ if (angular.isDate(baseDate) && !isNaN(baseDate.getTime()) && !tzOffset) {
+ dt = new Date(baseDate);
+ datesetter.call(dt, fields.year, fields.month, fields.date);
+ timesetter.call(dt, fields.hours, fields.minutes,
+ fields.seconds, fields.milliseconds);
+ } else {
+ dt = new Date(0);
+ datesetter.call(dt, fields.year, fields.month, fields.date);
+ timesetter.call(dt, fields.hours || 0, fields.minutes || 0,
+ fields.seconds || 0, fields.milliseconds || 0);
+ }
}
return dt;
@@ -162,14 +488,52 @@ angular.module('ui.bootstrap.dateparser', [])
return false;
}
- if ( month === 1 && date > 28) {
- return date === 29 && ((year % 4 === 0 && year % 100 !== 0) || year % 400 === 0);
+ 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;
+ if (month === 3 || month === 5 || month === 8 || month === 10) {
+ return date < 31;
}
return true;
}
+
+ function toInt(str) {
+ return parseInt(str, 10);
+ }
+
+ this.toTimezone = toTimezone;
+ this.fromTimezone = fromTimezone;
+ this.timezoneToOffset = timezoneToOffset;
+ this.addDateMinutes = addDateMinutes;
+ this.convertTimezoneToLocal = convertTimezoneToLocal;
+
+ function toTimezone(date, timezone) {
+ return date && timezone ? convertTimezoneToLocal(date, timezone) : date;
+ }
+
+ function fromTimezone(date, timezone) {
+ return date && timezone ? convertTimezoneToLocal(date, timezone, true) : date;
+ }
+
+ //https://github.com/angular/angular.js/blob/622c42169699ec07fc6daaa19fe6d224e5d2f70e/src/Angular.js#L1207
+ function timezoneToOffset(timezone, fallback) {
+ timezone = timezone.replace(/:/g, '');
+ var requestedTimezoneOffset = Date.parse('Jan 01, 1970 00:00:00 ' + timezone) / 60000;
+ return isNaN(requestedTimezoneOffset) ? fallback : requestedTimezoneOffset;
+ }
+
+ function addDateMinutes(date, minutes) {
+ date = new Date(date.getTime());
+ date.setMinutes(date.getMinutes() + minutes);
+ return date;
+ }
+
+ function convertTimezoneToLocal(date, timezone, reverse) {
+ reverse = reverse ? -1 : 1;
+ var dateTimezoneOffset = date.getTimezoneOffset();
+ var timezoneOffset = timezoneToOffset(timezone, dateTimezoneOffset);
+ return addDateMinutes(date, reverse * (timezoneOffset - dateTimezoneOffset));
+ }
}]);
diff --git a/src/dateparser/docs/README.md b/src/dateparser/docs/README.md
new file mode 100644
index 0000000000..f7a3b3434f
--- /dev/null
+++ b/src/dateparser/docs/README.md
@@ -0,0 +1,147 @@
+The `uibDateParser` is what the `uib-datepicker` uses internally to parse the dates. You can use it standalone by injecting the `uibDateParser` service where you need it.
+
+The public API for the dateParser is a single method called `parse`.
+
+Certain format codes support i18n. Check this [guide](https://docs.angularjs.org/guide/i18n) for more information.
+
+### uibDateParser's parse function
+
+##### parameters
+
+* `input`
+ _(Type: `string`, Example: `2004/Sep/4`)_ -
+ The input date to parse.
+
+* `format`
+ _(Type: `string`, Example: `yyyy/MMM/d`)_ -
+ The format we want to use. Check all the supported formats below.
+
+* `baseDate`
+ _(Type: `Date`, Example: `new Date()`)_ -
+ If you want to parse a date but maintain the timezone, you can pass an existing date here.
+
+##### return
+
+* If the specified input matches the format, a new date with the input will be returned, otherwise, it will return undefined.
+
+### uibDateParser's format codes
+
+* `yyyy`
+ _(Example: `2015`)_ -
+ Parses a 4 digits year.
+
+* `yy`
+ _(Example: `15`)_ -
+ Parses a 2 digits year.
+
+* `y`
+ _(Example: `15`)_ -
+ Parses a year with 1, 2, 3, or 4 digits.
+
+* `MMMM`
+ _(Example: `February`, i18n support)_ -
+ Parses the full name of a month.
+
+* `MMM`
+ _(Example: `Feb`, i18n support)_ -
+ Parses the short name of a month.
+
+* `MM`
+ _(Example: `12`, Leading 0)_ -
+ Parses a numeric month.
+
+* `M`
+ _(Example: `3`)_ -
+ Parses a numeric month.
+
+* `M!`
+ _(Example: `3` or `03`)_ -
+ Parses a numeric month, but allowing an optional leading zero
+
+* `LLLL`
+ _(Example: `February`, i18n support)_ - Stand-alone month in year (January-December). Requires Angular version 1.5.1 or higher.
+
+* `dd`
+ _(Example: `05`, Leading 0)_ -
+ Parses a numeric day.
+
+* `d`
+ _(Example: `5`)_ -
+ Parses a numeric day.
+
+* `d!`
+ _(Example: `3` or `03`)_ -
+ Parses a numeric day, but allowing an optional leading zero
+
+* `EEEE`
+ _(Example: `Sunday`, i18n support)_ -
+ Parses the full name of a day.
+
+* `EEE`
+ _(Example: `Mon`, i18n support)_ -
+ Parses the short name of a day.
+
+* `HH`
+ _(Example: `14`, Leading 0)_ -
+ Parses a 24 hours time.
+
+* `H`
+ _(Example: `3`)_ -
+ Parses a 24 hours time.
+
+* `hh`
+ _(Example: `11`, Leading 0)_ -
+ Parses a 12 hours time.
+
+* `h`
+ _(Example: `3`)_ -
+ Parses a 12 hours time.
+
+* `mm`
+ _(Example: `09`, Leading 0)_ -
+ Parses the minutes.
+
+* `m`
+ _(Example: `3`)_ -
+ Parses the minutes.
+
+* `sss`
+ _(Example: `094`, Leading 0)_ -
+ Parses the milliseconds.
+
+* `ss`
+ _(Example: `08`, Leading 0)_ -
+ Parses the seconds.
+
+* `s`
+ _(Example: `22`)_ -
+ Parses the seconds.
+
+* `a`
+ _(Example: `10AM`)_ -
+ Parses a 12 hours time with AM/PM.
+
+* `Z`
+ _(Example: `-0800`)_ -
+ Parses the timezone offset in a signed 4 digit representation
+
+* `ww`
+ _(Example: `03`, Leading 0)_ -
+ Parses the week number
+
+* `w`
+ _(Example: `03`)_ -
+ Parses the week number
+
+* `G`, `GG`, `GGG`
+ _(Example: `AD`)_ -
+ Parses the era (`AD` or `BC`)
+* `GGGG`
+ _(Example: `Anno Domini`)_ -
+ Parses the long form of the era (`Anno Domini` or `Before Christ`)
+
+\* The ones marked with `Leading 0`, needs a leading 0 for values less than 10. Exception being milliseconds which needs it for values under 100.
+
+\** It also supports `fullDate|longDate|medium|mediumDate|mediumTime|short|shortDate|shortTime` as the format for parsing.
+
+\*** It supports template literals as a string between the single quote `'` character, i.e. `'The Date is' MM/DD/YYYY`. If one wants the literal single quote character, one must use `''''`.
diff --git a/src/dateparser/docs/demo.html b/src/dateparser/docs/demo.html
new file mode 100644
index 0000000000..2467d43206
--- /dev/null
+++ b/src/dateparser/docs/demo.html
@@ -0,0 +1,11 @@
+
+
Formatting codes playground
+
+
+
+
+
+
+
+
+
diff --git a/src/dateparser/docs/demo.js b/src/dateparser/docs/demo.js
new file mode 100644
index 0000000000..df9976ea99
--- /dev/null
+++ b/src/dateparser/docs/demo.js
@@ -0,0 +1,4 @@
+angular.module('ui.bootstrap.demo').controller('DateParserDemoCtrl', function ($scope, uibDateParser) {
+ $scope.format = 'yyyy/MM/dd';
+ $scope.date = new Date();
+});
diff --git a/src/dateparser/index.js b/src/dateparser/index.js
new file mode 100644
index 0000000000..2320dec7d8
--- /dev/null
+++ b/src/dateparser/index.js
@@ -0,0 +1,7 @@
+require('./dateparser');
+
+var MODULE_NAME = 'ui.bootstrap.module.dateparser';
+
+angular.module(MODULE_NAME, ['ui.bootstrap.dateparser']);
+
+module.exports = MODULE_NAME;
diff --git a/src/dateparser/test/dateparser.spec.js b/src/dateparser/test/dateparser.spec.js
index 7ed6bb0e58..70c4b3e4f8 100644
--- a/src/dateparser/test/dateparser.spec.js
+++ b/src/dateparser/test/dateparser.spec.js
@@ -1,15 +1,282 @@
-describe('date parser', function () {
- var dateParser;
+describe('date parser', function() {
+ var dateParser, oldDate;
beforeEach(module('ui.bootstrap.dateparser'));
- beforeEach(inject(function (_dateParser_) {
- dateParser = _dateParser_;
+ beforeEach(inject(function (uibDateParser) {
+ dateParser = uibDateParser;
+ oldDate = new Date(1, 2, 6);
+ oldDate.setFullYear(1);
}));
+ function expectFilter(date, format, display) {
+ expect(dateParser.filter(date, format)).toEqual(display);
+ }
+
function expectParse(input, format, date) {
expect(dateParser.parse(input, format)).toEqual(date);
}
+ function expectBaseParse(input, format, baseDate, date) {
+ expect(dateParser.parse(input, format, baseDate)).toEqual(date);
+ }
+
+ describe('filter', function() {
+ it('should work correctly for `dd`, `MM`, `yyyy`', function() {
+ expectFilter(new Date(2013, 10, 17, 0), 'dd.MM.yyyy', '17.11.2013');
+ expectFilter(new Date(2013, 11, 31, 0), 'dd.MM.yyyy', '31.12.2013');
+ expectFilter(new Date(1991, 2, 8, 0), 'dd-MM-yyyy', '08-03-1991');
+ expectFilter(new Date(1980, 2, 5, 0), 'MM/dd/yyyy', '03/05/1980');
+ expectFilter(new Date(1983, 0, 10, 0), 'dd.MM/yyyy', '10.01/1983');
+ expectFilter(new Date(1980, 10, 9, 0), 'MM-dd-yyyy', '11-09-1980');
+ expectFilter(new Date(2011, 1, 5, 0), 'yyyy/MM/dd', '2011/02/05');
+ expectFilter(oldDate, 'yyyy/MM/dd', '0001/03/06');
+ });
+
+ it('should work correctly for `yy`', function() {
+ expectFilter(new Date(2013, 10, 17, 0), 'dd.MM.yy', '17.11.13');
+ expectFilter(new Date(2011, 4, 2, 0), 'dd-MM-yy', '02-05-11');
+ expectFilter(new Date(2080, 1, 5, 0), 'MM/dd/yy', '02/05/80');
+ expectFilter(new Date(2055, 1, 5, 0), 'yy/MM/dd', '55/02/05');
+ expectFilter(new Date(2013, 7, 11, 0), 'dd-MM-yy', '11-08-13');
+ });
+
+ it('should work correctly for `y`', function() {
+ expectFilter(new Date(2013, 10, 17, 0), 'dd.MM.y', '17.11.2013');
+ expectFilter(new Date(2013, 11, 31, 0), 'dd.MM.y', '31.12.2013');
+ expectFilter(new Date(1991, 2, 8, 0), 'dd-MM-y', '08-03-1991');
+ expectFilter(new Date(1980, 2, 5, 0), 'MM/dd/y', '03/05/1980');
+ expectFilter(new Date(1983, 0, 10, 0), 'dd.MM/y', '10.01/1983');
+ expectFilter(new Date(1980, 10, 9, 0), 'MM-dd-y', '11-09-1980');
+ expectFilter(new Date(2011, 1, 5, 0), 'y/MM/dd', '2011/02/05');
+ });
+
+ it('should work correctly for `MMMM`', function() {
+ expectFilter(new Date(2013, 10, 17, 0), 'dd.MMMM.yy', '17.November.13');
+ expectFilter(new Date(1980, 2, 5, 0), 'dd-MMMM-yyyy', '05-March-1980');
+ expectFilter(new Date(1980, 1, 5, 0), 'MMMM/dd/yyyy', 'February/05/1980');
+ expectFilter(new Date(1949, 11, 20, 0), 'yyyy/MMMM/dd', '1949/December/20');
+ expectFilter(oldDate, 'yyyy/MMMM/dd', '0001/March/06');
+ });
+
+ it('should work correctly for `MMM`', function() {
+ expectFilter(new Date(2010, 8, 30, 0), 'dd.MMM.yy', '30.Sep.10');
+ expectFilter(new Date(2011, 4, 2, 0), 'dd-MMM-yy', '02-May-11');
+ expectFilter(new Date(1980, 1, 5, 0), 'MMM/dd/yyyy', 'Feb/05/1980');
+ expectFilter(new Date(1955, 1, 5, 0), 'yyyy/MMM/dd', '1955/Feb/05');
+ expectFilter(oldDate, 'yyyy/MMM/dd', '0001/Mar/06');
+ });
+
+ it('should work correctly for `M`', function() {
+ expectFilter(new Date(2013, 7, 11, 0), 'M/dd/yyyy', '8/11/2013');
+ expectFilter(new Date(2005, 10, 7, 0), 'dd.M.yy', '07.11.05');
+ expectFilter(new Date(2011, 4, 2, 0), 'dd-M-yy', '02-5-11');
+ expectFilter(new Date(1980, 1, 5, 0), 'M/dd/yyyy', '2/05/1980');
+ expectFilter(new Date(1955, 1, 5, 0), 'yyyy/M/dd', '1955/2/05');
+ expectFilter(new Date(2011, 4, 2, 0), 'dd-M-yy', '02-5-11');
+ });
+
+ it('should work correctly for `M!`', function() {
+ expectFilter(new Date(2013, 7, 11, 0), 'M!/dd/yyyy', '08/11/2013');
+ expectFilter(new Date(2005, 10, 7, 0), 'dd.M!.yy', '07.11.05');
+ expectFilter(new Date(2011, 4, 2, 0), 'dd-M!-yy', '02-05-11');
+ expectFilter(new Date(1980, 1, 5, 0), 'M!/dd/yyyy', '02/05/1980');
+ expectFilter(new Date(1955, 1, 5, 0), 'yyyy/M!/dd', '1955/02/05');
+ expectFilter(new Date(2011, 4, 2, 0), 'dd-M!-yy', '02-05-11');
+ expectFilter(oldDate, 'yyyy/M!/dd', '0001/03/06');
+ });
+
+ it('should work correctly for `LLLL`', function() {
+ expectFilter(new Date(2013, 7, 24, 0), 'LLLL/dd/yyyy', 'August/24/2013');
+ expectFilter(new Date(2004, 10, 7, 0), 'dd.LLLL.yy', '07.November.04');
+ expectFilter(new Date(2011, 4, 18, 0), 'dd-LLLL-yy', '18-May-11');
+ expectFilter(new Date(1980, 1, 5, 0), 'LLLL/dd/yyyy', 'February/05/1980');
+ expectFilter(new Date(1955, 2, 5, 0), 'yyyy/LLLL/dd', '1955/March/05');
+ expectFilter(new Date(2011, 5, 2, 0), 'dd-LLLL-yy', '02-June-11');
+ expectFilter(oldDate, 'yyyy/LLLL/dd', '0001/March/06');
+ });
+
+ it('should work correctly for `d`', function() {
+ expectFilter(new Date(2013, 10, 17, 0), 'd.MMMM.yy', '17.November.13');
+ expectFilter(new Date(1991, 2, 8, 0), 'd-MMMM-yyyy', '8-March-1991');
+ expectFilter(new Date(1980, 1, 5, 0), 'MMMM/d/yyyy', 'February/5/1980');
+ expectFilter(new Date(1955, 1, 5, 0), 'yyyy/MMMM/d', '1955/February/5');
+ expectFilter(new Date(2013, 7, 11, 0), 'd-MM-yy', '11-08-13');
+ expectFilter(oldDate, 'yyyy/MM/d', '0001/03/6');
+ });
+
+ it('should work correctly for `d!`', function() {
+ expectFilter(new Date(2013, 10, 17, 0), 'd!.MMMM.yy', '17.November.13');
+ expectFilter(new Date(1991, 2, 8, 0), 'd!-MMMM-yyyy', '08-March-1991');
+ expectFilter(new Date(1980, 1, 5, 0), 'MMMM/d!/yyyy', 'February/05/1980');
+ expectFilter(new Date(1955, 1, 5, 0), 'yyyy/MMMM/d!', '1955/February/05');
+ expectFilter(new Date(2013, 7, 11, 0), 'd!-MM-yy', '11-08-13');
+ expectFilter(oldDate, 'yyyy/MM/d!', '0001/03/06');
+ });
+
+ it('should work correctly for `EEEE`', function() {
+ expectFilter(new Date(2013, 10, 17, 0), 'EEEE.d.MMMM.yy', 'Sunday.17.November.13');
+ expectFilter(new Date(1991, 2, 8, 0), 'd-EEEE-MMMM-yyyy', '8-Friday-March-1991');
+ expectFilter(new Date(1980, 1, 5, 0), 'MMMM/d/yyyy/EEEE', 'February/5/1980/Tuesday');
+ expectFilter(new Date(1955, 1, 5, 0), 'yyyy/EEEE/MMMM/d', '1955/Saturday/February/5');
+ });
+
+ it('should work correctly for `EEE`', function() {
+ expectFilter(new Date(2013, 10, 17, 0), 'EEE.d.MMMM.yy', 'Sun.17.November.13');
+ expectFilter(new Date(1991, 2, 8, 0), 'd-EEE-MMMM-yyyy', '8-Fri-March-1991');
+ expectFilter(new Date(1980, 1, 5, 0), 'MMMM/d/yyyy/EEE', 'February/5/1980/Tue');
+ expectFilter(new Date(1955, 1, 5, 0), 'yyyy/EEE/MMMM/d', '1955/Sat/February/5');
+ });
+
+ it('should work correctly for `HH`', function() {
+ expectFilter(new Date(2015, 2, 22, 22), 'd.MMMM.yy.HH', '22.March.15.22');
+ expectFilter(new Date(1991, 2, 8, 11), 'd-MMMM-yyyy-HH', '8-March-1991-11');
+ expectFilter(new Date(1980, 1, 5, 0), 'MMMM/d/yyyy/HH', 'February/5/1980/00');
+ expectFilter(new Date(1955, 1, 5, 3), 'yyyy/MMMM/d HH', '1955/February/5 03');
+ expectFilter(new Date(2013, 7, 11, 23), 'd-MM-yy HH', '11-08-13 23');
+ });
+
+ it('should work correctly for `H`', function() {
+ expectFilter(new Date(2015, 2, 22, 22), 'd.MMMM.yy.H', '22.March.15.22');
+ expectFilter(new Date(1991, 2, 8, 11), 'd-MMMM-yyyy-H', '8-March-1991-11');
+ expectFilter(new Date(1980, 1, 5, 0), 'MMMM/d/yyyy/H', 'February/5/1980/0');
+ expectFilter(new Date(1955, 1, 5, 3), 'yyyy/MMMM/d H', '1955/February/5 3');
+ expectFilter(new Date(2013, 7, 11, 23), 'd-MM-yy H', '11-08-13 23');
+ });
+
+ it('should work correctly for `hh`', function() {
+ expectFilter(new Date(2015, 2, 22, 12), 'd.MMMM.yy.hh', '22.March.15.12');
+ expectFilter(new Date(1991, 2, 8, 11), 'd-MMMM-yyyy-hh', '8-March-1991-11');
+ expectFilter(new Date(1980, 1, 5, 0), 'MMMM/d/yyyy/hh', 'February/5/1980/12');
+ expectFilter(new Date(1955, 1, 5, 3), 'yyyy/MMMM/d hh', '1955/February/5 03');
+ expectFilter(new Date(2013, 7, 11, 9), 'd-MM-yy hh', '11-08-13 09');
+ });
+
+ it('should work correctly for `h`', function() {
+ expectFilter(new Date(2015, 2, 22, 12), 'd.MMMM.yy.h', '22.March.15.12');
+ expectFilter(new Date(1991, 2, 8, 11), 'd-MMMM-yyyy-h', '8-March-1991-11');
+ expectFilter(new Date(1980, 1, 5, 0), 'MMMM/d/yyyy/h', 'February/5/1980/12');
+ expectFilter(new Date(1955, 1, 5, 3), 'yyyy/MMMM/d h', '1955/February/5 3');
+ expectFilter(new Date(2013, 7, 11, 3), 'd-MM-yy h', '11-08-13 3');
+ });
+
+ it('should work correctly for `mm`', function() {
+ expectFilter(new Date(2015, 2, 22, 0, 22), 'd.MMMM.yy.mm', '22.March.15.22');
+ expectFilter(new Date(1991, 2, 8, 0, 59), 'd-MMMM-yyyy-mm', '8-March-1991-59');
+ expectFilter(new Date(1980, 1, 5, 0, 0), 'MMMM/d/yyyy/mm', 'February/5/1980/00');
+ expectFilter(new Date(1955, 1, 5, 0, 3), 'yyyy/MMMM/d mm', '1955/February/5 03');
+ expectFilter(new Date(2013, 7, 11, 0, 46), 'd-MM-yy mm', '11-08-13 46');
+ expectFilter(new Date(2015, 2, 22, 22, 33), 'd.MMMM.yy.HH:mm', '22.March.15.22:33');
+ expectFilter(new Date(2015, 2, 22, 2, 1), 'd.MMMM.yy.H:mm', '22.March.15.2:01');
+ });
+
+ it('should work correctly for `m`', function() {
+ expectFilter(new Date(2015, 2, 22, 0, 22), 'd.MMMM.yy.m', '22.March.15.22');
+ expectFilter(new Date(1991, 2, 8, 0, 59), 'd-MMMM-yyyy-m', '8-March-1991-59');
+ expectFilter(new Date(1980, 1, 5, 0, 0), 'MMMM/d/yyyy/m', 'February/5/1980/0');
+ expectFilter(new Date(1955, 1, 5, 0, 3), 'yyyy/MMMM/d m', '1955/February/5 3');
+ expectFilter(new Date(2013, 7, 11, 0, 46), 'd-MM-yy m', '11-08-13 46');
+ expectFilter(new Date(2015, 2, 22, 22, 3), 'd.MMMM.yy.HH:m', '22.March.15.22:3');
+ expectFilter(new Date(2015, 2, 22, 2, 1), 'd.MMMM.yy.H:m', '22.March.15.2:1');
+ });
+
+ it('should work correctly for `sss`', function() {
+ expectFilter(new Date(2015, 2, 22, 0, 0, 0, 123), 'd.MMMM.yy.sss', '22.March.15.123');
+ expectFilter(new Date(1991, 2, 8, 0, 0, 0, 59), 'd-MMMM-yyyy-sss', '8-March-1991-059');
+ expectFilter(new Date(1980, 1, 5, 0, 0, 0), 'MMMM/d/yyyy/sss', 'February/5/1980/000');
+ expectFilter(new Date(1955, 1, 5, 0, 0, 0, 3), 'yyyy/MMMM/d sss', '1955/February/5 003');
+ expectFilter(new Date(2013, 7, 11, 0, 0, 0, 46), 'd-MM-yy sss', '11-08-13 046');
+ expectFilter(new Date(2015, 2, 22, 22, 33, 0, 44), 'd.MMMM.yy.HH:mm:sss', '22.March.15.22:33:044');
+ expectFilter(new Date(2015, 2, 22, 0, 0, 0, 1), 'd.MMMM.yy.H:m:sss', '22.March.15.0:0:001');
+ });
+
+ it('should work correctly for `ss`', function() {
+ expectFilter(new Date(2015, 2, 22, 0, 0, 22), 'd.MMMM.yy.ss', '22.March.15.22');
+ expectFilter(new Date(1991, 2, 8, 0, 0, 59), 'd-MMMM-yyyy-ss', '8-March-1991-59');
+ expectFilter(new Date(1980, 1, 5, 0, 0, 0), 'MMMM/d/yyyy/ss', 'February/5/1980/00');
+ expectFilter(new Date(1955, 1, 5, 0, 0, 3), 'yyyy/MMMM/d ss', '1955/February/5 03');
+ expectFilter(new Date(2013, 7, 11, 0, 0, 46), 'd-MM-yy ss', '11-08-13 46');
+ expectFilter(new Date(2015, 2, 22, 22, 33, 44), 'd.MMMM.yy.HH:mm:ss', '22.March.15.22:33:44');
+ expectFilter(new Date(2015, 2, 22, 0, 0, 1), 'd.MMMM.yy.H:m:ss', '22.March.15.0:0:01');
+ });
+
+ it('should work correctly for `s`', function() {
+ expectFilter(new Date(2015, 2, 22, 0, 0, 22), 'd.MMMM.yy.s', '22.March.15.22');
+ expectFilter(new Date(1991, 2, 8, 0, 0, 59), 'd-MMMM-yyyy-s', '8-March-1991-59');
+ expectFilter(new Date(1980, 1, 5, 0, 0, 0), 'MMMM/d/yyyy/s', 'February/5/1980/0');
+ expectFilter(new Date(1955, 1, 5, 0, 0, 3), 'yyyy/MMMM/d s', '1955/February/5 3');
+ expectFilter(new Date(2013, 7, 11, 0, 0, 46), 'd-MM-yy s', '11-08-13 46');
+ expectFilter(new Date(2015, 2, 22, 22, 33, 4), 'd.MMMM.yy.HH:mm:s', '22.March.15.22:33:4');
+ expectFilter(new Date(2015, 2, 22, 22, 3, 4), 'd.MMMM.yy.HH:m:s', '22.March.15.22:3:4');
+ });
+
+ it('should work correctly for `a`', function() {
+ expectFilter(new Date(2015, 2, 22, 10), 'd.MMMM.yy.hha', '22.March.15.10AM');
+ expectFilter(new Date(2015, 2, 22, 22), 'd.MMMM.yy.hha', '22.March.15.10PM');
+ expectFilter(new Date(1991, 2, 8, 11), 'd-MMMM-yyyy-hha', '8-March-1991-11AM');
+ expectFilter(new Date(1991, 2, 8, 23), 'd-MMMM-yyyy-hha', '8-March-1991-11PM');
+ expectFilter(new Date(1980, 1, 5, 0), 'MMMM/d/yyyy/hha', 'February/5/1980/12AM');
+ expectFilter(new Date(1980, 1, 5, 12), 'MMMM/d/yyyy/hha', 'February/5/1980/12PM');
+ expectFilter(new Date(1955, 1, 5, 3), 'yyyy/MMMM/d hha', '1955/February/5 03AM');
+ expectFilter(new Date(1955, 1, 5, 15), 'yyyy/MMMM/d hha', '1955/February/5 03PM');
+ expectFilter(new Date(2013, 7, 11, 9), 'd-MM-yy hha', '11-08-13 09AM');
+ expectFilter(new Date(2013, 7, 11, 21), 'd-MM-yy hha', '11-08-13 09PM');
+ });
+
+ it('should work correctly for `ww`', function() {
+ expectFilter(new Date(2013, 10, 17, 0), 'd.MMMM.yy.ww', '17.November.13.47');
+ expectFilter(new Date(1991, 2, 8, 0), 'd-MMMM-yyyy-ww', '8-March-1991-10');
+ expectFilter(new Date(1980, 1, 5, 0), 'MMMM/d/yyyy/ww', 'February/5/1980/06');
+ expectFilter(new Date(1955, 1, 5, 0), 'yyyy/MMMM/d/ww', '1955/February/5/05');
+ expectFilter(new Date(2013, 7, 11, 0), 'd-MM-yy ww', '11-08-13 33');
+ expectFilter(oldDate, 'yyyy/MM/d ww', '0001/03/6 10');
+ });
+
+ it('should work correctly for `w`', function() {
+ expectFilter(new Date(2013, 10, 17, 0), 'd.MMMM.yy.w', '17.November.13.47');
+ expectFilter(new Date(1991, 2, 8, 0), 'd-MMMM-yyyy-w', '8-March-1991-10');
+ expectFilter(new Date(1980, 1, 5, 0), 'MMMM/d/yyyy/w', 'February/5/1980/6');
+ expectFilter(new Date(1955, 1, 5, 0), 'yyyy/MMMM/d/w', '1955/February/5/5');
+ expectFilter(new Date(2013, 7, 11, 0), 'd-MM-yy w', '11-08-13 33');
+ expectFilter(oldDate, 'yyyy/MM/d w', '0001/03/6 10');
+ });
+
+ it('should work correctly for `G`', function() {
+ expectFilter(new Date(2013, 10, 17, 0), 'd.MMMM.yy.G', '17.November.13.AD');
+ expectFilter(new Date(-1991, 2, 8, 0), 'd-MMMM-yyyy-G', '8-March-1991-BC');
+ expectFilter(new Date(1980, 1, 5, 0), 'MMMM/d/yyyy/G', 'February/5/1980/AD');
+ expectFilter(new Date(-1955, 1, 5, 0), 'yyyy/MMMM/d/G', '1955/February/5/BC');
+ expectFilter(new Date(2013, 7, 11, 0), 'd-MM-yy G', '11-08-13 AD');
+ });
+
+ it('should work correctly for `GG`', function() {
+ expectFilter(new Date(2013, 10, 17, 0), 'd.MMMM.yy.GG', '17.November.13.AD');
+ expectFilter(new Date(-1991, 2, 8, 0), 'd-MMMM-yyyy-GG', '8-March-1991-BC');
+ expectFilter(new Date(1980, 1, 5, 0), 'MMMM/d/yyyy/GG', 'February/5/1980/AD');
+ expectFilter(new Date(-1955, 1, 5, 0), 'yyyy/MMMM/d/GG', '1955/February/5/BC');
+ expectFilter(new Date(2013, 7, 11, 0), 'd-MM-yy GG', '11-08-13 AD');
+ });
+
+ it('should work correctly for `GGG`', function() {
+ expectFilter(new Date(2013, 10, 17, 0), 'd.MMMM.yy.GGG', '17.November.13.AD');
+ expectFilter(new Date(-1991, 2, 8, 0), 'd-MMMM-yyyy-GGG', '8-March-1991-BC');
+ expectFilter(new Date(1980, 1, 5, 0), 'MMMM/d/yyyy/GGG', 'February/5/1980/AD');
+ expectFilter(new Date(-1955, 1, 5, 0), 'yyyy/MMMM/d/GGG', '1955/February/5/BC');
+ expectFilter(new Date(2013, 7, 11, 0), 'd-MM-yy GGG', '11-08-13 AD');
+ });
+
+ it('should work correctly for `GGGG`', function() {
+ expectFilter(new Date(2013, 10, 17, 0), 'd.MMMM.yy.GGGG', '17.November.13.Anno Domini');
+ expectFilter(new Date(-1991, 2, 8, 0), 'd-MMMM-yyyy-GGGG', '8-March-1991-Before Christ');
+ expectFilter(new Date(1980, 1, 5, 0), 'MMMM/d/yyyy/GGGG', 'February/5/1980/Anno Domini');
+ expectFilter(new Date(-1955, 1, 5, 0), 'yyyy/MMMM/d/GGGG', '1955/February/5/Before Christ');
+ expectFilter(new Date(2013, 7, 11, 0), 'd-MM-yy GGGG', '11-08-13 Anno Domini');
+ });
+
+ it('should work correctly for literal text', function() {
+ expectFilter(new Date(2013, 10, 17, 0), 'dd.MM.yyyy foo', '17.11.2013 foo');
+ });
+ });
+
describe('with custom formats', function() {
it('should work correctly for `dd`, `MM`, `yyyy`', function() {
expectParse('17.11.2013', 'dd.MM.yyyy', new Date(2013, 10, 17, 0));
@@ -19,16 +286,48 @@ describe('date parser', function () {
expectParse('10.01/1983', 'dd.MM/yyyy', new Date(1983, 0, 10, 0));
expectParse('11-09-1980', 'MM-dd-yyyy', new Date(1980, 10, 9, 0));
expectParse('2011/02/05', 'yyyy/MM/dd', new Date(2011, 1, 5, 0));
+ expectParse('0001/03/06', 'yyyy/MM/dd', oldDate);
});
it('should work correctly for `yy`', function() {
expectParse('17.11.13', 'dd.MM.yy', new Date(2013, 10, 17, 0));
expectParse('02-05-11', 'dd-MM-yy', new Date(2011, 4, 2, 0));
- expectParse('02/05/80', 'MM/dd/yy', new Date(2080, 1, 5, 0));
+ expectParse('02/05/80', 'MM/dd/yy', new Date(1980, 1, 5, 0));
expectParse('55/02/05', 'yy/MM/dd', new Date(2055, 1, 5, 0));
expectParse('11-08-13', 'dd-MM-yy', new Date(2013, 7, 11, 0));
});
+ it('should use `68` as the pivot year for `yy`', function() {
+ expectParse('17.11.68', 'dd.MM.yy', new Date(2068, 10, 17, 0));
+ expectParse('17.11.69', 'dd.MM.yy', new Date(1969, 10, 17, 0));
+ });
+
+ it('should work correctly for `y`', function() {
+ expectParse('17.11.2013', 'dd.MM.y', new Date(2013, 10, 17, 0));
+ expectParse('31.12.2013', 'dd.MM.y', new Date(2013, 11, 31, 0));
+ expectParse('08-03-1991', 'dd-MM-y', new Date(1991, 2, 8, 0));
+ expectParse('03/05/1980', 'MM/dd/y', new Date(1980, 2, 5, 0));
+ expectParse('10.01/1983', 'dd.MM/y', new Date(1983, 0, 10, 0));
+ expectParse('11-09-1980', 'MM-dd-y', new Date(1980, 10, 9, 0));
+ expectParse('2011/02/05', 'y/MM/dd', new Date(2011, 1, 5, 0));
+ });
+
+ it('should work correctly for `MMMM`', function() {
+ expectParse('17.November.13', 'dd.MMMM.yy', new Date(2013, 10, 17, 0));
+ expectParse('05-March-1980', 'dd-MMMM-yyyy', new Date(1980, 2, 5, 0));
+ expectParse('February/05/1980', 'MMMM/dd/yyyy', new Date(1980, 1, 5, 0));
+ expectParse('1949/December/20', 'yyyy/MMMM/dd', new Date(1949, 11, 20, 0));
+ expectParse('0001/March/06', 'yyyy/MMMM/dd', oldDate);
+ });
+
+ it('should work correctly for `MMM`', function() {
+ expectParse('30.Sep.10', 'dd.MMM.yy', new Date(2010, 8, 30, 0));
+ expectParse('02-May-11', 'dd-MMM-yy', new Date(2011, 4, 2, 0));
+ expectParse('Feb/05/1980', 'MMM/dd/yyyy', new Date(1980, 1, 5, 0));
+ expectParse('1955/Feb/05', 'yyyy/MMM/dd', new Date(1955, 1, 5, 0));
+ expectParse('0001/Mar/06', 'yyyy/MMM/dd', oldDate);
+ });
+
it('should work correctly for `M`', function() {
expectParse('8/11/2013', 'M/dd/yyyy', new Date(2013, 7, 11, 0));
expectParse('07.11.05', 'dd.M.yy', new Date(2005, 10, 7, 0));
@@ -38,18 +337,22 @@ describe('date parser', function () {
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 `M!`', function() {
+ expectParse('8/11/2013', 'M!/dd/yyyy', new Date(2013, 7, 11, 0));
+ expectParse('07.11.05', 'dd.M!.yy', new Date(2005, 10, 7, 0));
+ expectParse('02-5-11', 'dd-M!-yy', new Date(2011, 4, 2, 0));
+ expectParse('2/05/1980', 'M!/dd/yyyy', new Date(1980, 1, 5, 0));
+ expectParse('1955/2/05', 'yyyy/M!/dd', new Date(1955, 1, 5, 0));
+ expectParse('02-5-11', 'dd-M!-yy', new Date(2011, 4, 2, 0));
+ expectParse('0001/3/06', 'yyyy/M!/dd', oldDate);
- it('should work correctly for `MMMM`', function() {
- expectParse('17.November.13', 'dd.MMMM.yy', new Date(2013, 10, 17, 0));
- expectParse('05-March-1980', 'dd-MMMM-yyyy', new Date(1980, 2, 5, 0));
- expectParse('February/05/1980', 'MMMM/dd/yyyy', new Date(1980, 1, 5, 0));
- expectParse('1949/December/20', 'yyyy/MMMM/dd', new Date(1949, 11, 20, 0));
+ expectParse('08/11/2013', 'M!/dd/yyyy', new Date(2013, 7, 11, 0));
+ expectParse('07.11.05', 'dd.M!.yy', new Date(2005, 10, 7, 0));
+ expectParse('02-05-11', 'dd-M!-yy', new Date(2011, 4, 2, 0));
+ expectParse('02/05/1980', 'M!/dd/yyyy', new Date(1980, 1, 5, 0));
+ expectParse('1955/02/05', 'yyyy/M!/dd', new Date(1955, 1, 5, 0));
+ expectParse('02-05-11', 'dd-M!-yy', new Date(2011, 4, 2, 0));
+ expectParse('0001/03/06', 'yyyy/M!/dd', oldDate);
});
it('should work correctly for `d`', function() {
@@ -58,6 +361,37 @@ describe('date parser', function () {
expectParse('February/5/1980', 'MMMM/d/yyyy', new Date(1980, 1, 5, 0));
expectParse('1955/February/5', 'yyyy/MMMM/d', new Date(1955, 1, 5, 0));
expectParse('11-08-13', 'd-MM-yy', new Date(2013, 7, 11, 0));
+ expectParse('0001/03/6', 'yyyy/MM/d', oldDate);
+ });
+
+ it('should work correctly for `d!`', function() {
+ expectParse('17.November.13', 'd!.MMMM.yy', new Date(2013, 10, 17, 0));
+ expectParse('8-March-1991', 'd!-MMMM-yyyy', new Date(1991, 2, 8, 0));
+ expectParse('February/5/1980', 'MMMM/d!/yyyy', new Date(1980, 1, 5, 0));
+ expectParse('1955/February/5', 'yyyy/MMMM/d!', new Date(1955, 1, 5, 0));
+ expectParse('11-08-13', 'd!-MM-yy', new Date(2013, 7, 11, 0));
+ expectParse('0001/03/6', 'yyyy/MM/d!', oldDate);
+
+ expectParse('17.November.13', 'd!.MMMM.yy', new Date(2013, 10, 17, 0));
+ expectParse('08-March-1991', 'd!-MMMM-yyyy', new Date(1991, 2, 8, 0));
+ expectParse('February/05/1980', 'MMMM/d!/yyyy', new Date(1980, 1, 5, 0));
+ expectParse('1955/February/05', 'yyyy/MMMM/d!', new Date(1955, 1, 5, 0));
+ expectParse('11-08-13', 'd!-MM-yy', new Date(2013, 7, 11, 0));
+ expectParse('0001/03/06', 'yyyy/MM/d!', oldDate);
+ });
+
+ it('should work correctly for `EEEE`', function() {
+ expectParse('Sunday.17.November.13', 'EEEE.d.MMMM.yy', new Date(2013, 10, 17, 0));
+ expectParse('8-Friday-March-1991', 'd-EEEE-MMMM-yyyy', new Date(1991, 2, 8, 0));
+ expectParse('February/5/1980/Tuesday', 'MMMM/d/yyyy/EEEE', new Date(1980, 1, 5, 0));
+ expectParse('1955/Saturday/February/5', 'yyyy/EEEE/MMMM/d', new Date(1955, 1, 5, 0));
+ });
+
+ it('should work correctly for `EEE`', function() {
+ expectParse('Sun.17.November.13', 'EEE.d.MMMM.yy', new Date(2013, 10, 17, 0));
+ expectParse('8-Fri-March-1991', 'd-EEE-MMMM-yyyy', new Date(1991, 2, 8, 0));
+ expectParse('February/5/1980/Tue', 'MMMM/d/yyyy/EEE', new Date(1980, 1, 5, 0));
+ expectParse('1955/Sat/February/5', 'yyyy/EEE/MMMM/d', new Date(1955, 1, 5, 0));
});
it('should work correctly for `HH`', function() {
@@ -76,6 +410,24 @@ describe('date parser', function () {
expectParse('11-08-13 23', 'd-MM-yy H', new Date(2013, 7, 11, 23));
});
+ it('should work correctly for `hh`', function() {
+ expectParse('22.March.15.22', 'd.MMMM.yy.hh', undefined);
+ expectParse('22.March.15.12', 'd.MMMM.yy.hh', new Date(2015, 2, 22, 12));
+ expectParse('8-March-1991-11', 'd-MMMM-yyyy-hh', new Date(1991, 2, 8, 11));
+ expectParse('February/5/1980/00', 'MMMM/d/yyyy/hh', new Date(1980, 1, 5, 0));
+ expectParse('1955/February/5 03', 'yyyy/MMMM/d hh', new Date(1955, 1, 5, 3));
+ expectParse('11-08-13 23', 'd-MM-yy hh', undefined);
+ expectParse('11-08-13 09', 'd-MM-yy hh', new Date(2013, 7, 11, 9));
+ });
+
+ it('should work correctly for `h`', function() {
+ expectParse('22.March.15.12', 'd.MMMM.yy.h', new Date(2015, 2, 22, 12));
+ expectParse('8-March-1991-11', 'd-MMMM-yyyy-h', new Date(1991, 2, 8, 11));
+ expectParse('February/5/1980/0', 'MMMM/d/yyyy/h', new Date(1980, 1, 5, 0));
+ expectParse('1955/February/5 3', 'yyyy/MMMM/d h', new Date(1955, 1, 5, 3));
+ expectParse('11-08-13 3', 'd-MM-yy h', new Date(2013, 7, 11, 3));
+ });
+
it('should work correctly for `mm`', function() {
expectParse('22.March.15.22', 'd.MMMM.yy.mm', new Date(2015, 2, 22, 0, 22));
expectParse('8-March-1991-59', 'd-MMMM-yyyy-mm', new Date(1991, 2, 8, 0, 59));
@@ -125,6 +477,83 @@ describe('date parser', function () {
expectParse('22.March.15.22:33:4', 'd.MMMM.yy.HH:mm:s', new Date(2015, 2, 22, 22, 33, 4));
expectParse('22.March.15.22:3:4', 'd.MMMM.yy.HH:m:s', new Date(2015, 2, 22, 22, 3, 4));
});
+
+ it('should work correctly for `a`', function() {
+ expectParse('22.March.15.10AM', 'd.MMMM.yy.hha', new Date(2015, 2, 22, 10));
+ expectParse('22.March.15.10PM', 'd.MMMM.yy.hha', new Date(2015, 2, 22, 22));
+ expectParse('8-March-1991-11AM', 'd-MMMM-yyyy-hha', new Date(1991, 2, 8, 11));
+ expectParse('8-March-1991-11PM', 'd-MMMM-yyyy-hha', new Date(1991, 2, 8, 23));
+ expectParse('February/5/1980/12AM', 'MMMM/d/yyyy/hha', new Date(1980, 1, 5, 0));
+ expectParse('February/5/1980/12PM', 'MMMM/d/yyyy/hha', new Date(1980, 1, 5, 12));
+ expectParse('1955/February/5 03AM', 'yyyy/MMMM/d hha', new Date(1955, 1, 5, 3));
+ expectParse('1955/February/5 03PM', 'yyyy/MMMM/d hha', new Date(1955, 1, 5, 15));
+ expectParse('11-08-13 09AM', 'd-MM-yy hha', new Date(2013, 7, 11, 9));
+ expectParse('11-08-13 09PM', 'd-MM-yy hha', new Date(2013, 7, 11, 21));
+ });
+
+ it('should work correctly for `Z`', function() {
+ expectParse('22.March.15 -0700', 'd.MMMM.yy Z', new Date(2015, 2, 21, 17, 0, 0));
+ expectParse('8-March-1991 +0800', 'd-MMMM-yyyy Z', new Date(1991, 2, 8, 8, 0, 0));
+ expectParse('February/5/1980 -0200', 'MMMM/d/yyyy Z', new Date(1980, 1, 4, 22, 0, 0));
+ expectParse('1955/February/5 +0400', 'yyyy/MMMM/d Z', new Date(1955, 1, 5, 4, 0, 0));
+ expectParse('11-08-13 -1234', 'd-MM-yy Z', new Date(2013, 7, 10, 11, 26, 0));
+ expectParse('22.March.15.22:33:4 -1200', 'd.MMMM.yy.HH:mm:s Z', new Date(2015, 2, 22, 10, 33, 4));
+ expectParse('22.March.15.22:3:4 +1500', 'd.MMMM.yy.HH:m:s Z', new Date(2015, 2, 23, 13, 3, 4));
+ });
+
+ it('should work correctly for `ww`', function() {
+ expectParse('17.November.13.45', 'd.MMMM.yy.ww', new Date(2013, 10, 17, 0));
+ expectParse('8-March-1991-09', 'd-MMMM-yyyy-ww', new Date(1991, 2, 8, 0));
+ expectParse('February/5/1980/05', 'MMMM/d/yyyy/ww', new Date(1980, 1, 5, 0));
+ expectParse('1955/February/5/04', 'yyyy/MMMM/d/ww', new Date(1955, 1, 5, 0));
+ expectParse('11-08-13 44', 'd-MM-yy ww', new Date(2013, 7, 11, 0));
+ expectParse('0001/03/6 10', 'yyyy/MM/d ww', oldDate);
+ });
+
+ it('should work correctly for `w`', function() {
+ expectParse('17.November.13.45', 'd.MMMM.yy.w', new Date(2013, 10, 17, 0));
+ expectParse('8-March-1991-9', 'd-MMMM-yyyy-w', new Date(1991, 2, 8, 0));
+ expectParse('February/5/1980/5', 'MMMM/d/yyyy/w', new Date(1980, 1, 5, 0));
+ expectParse('1955/February/5/4', 'yyyy/MMMM/d/w', new Date(1955, 1, 5, 0));
+ expectParse('11-08-13 44', 'd-MM-yy w', new Date(2013, 7, 11, 0));
+ expectParse('0001/03/6 10', 'yyyy/MM/d w', oldDate);
+ });
+
+ it('should work correctly for `G`', function() {
+ expectParse('17.November.13.AD', 'd.MMMM.yy.G', new Date(2013, 10, 17, 0));
+ expectParse('8-March-1991-BC', 'd-MMMM-yyyy-G', new Date(1991, 2, 8, 0));
+ expectParse('February/5/1980/AD', 'MMMM/d/yyyy/G', new Date(1980, 1, 5, 0));
+ expectParse('1955/February/5/BC', 'yyyy/MMMM/d/G', new Date(1955, 1, 5, 0));
+ expectParse('11-08-13 AD', 'd-MM-yy G', new Date(2013, 7, 11, 0));
+ expectParse('0001/03/6 BC', 'yyyy/MM/d G', oldDate);
+ });
+
+ it('should work correctly for `GG`', function() {
+ expectParse('17.November.13.AD', 'd.MMMM.yy.GG', new Date(2013, 10, 17, 0));
+ expectParse('8-March-1991-BC', 'd-MMMM-yyyy-GG', new Date(1991, 2, 8, 0));
+ expectParse('February/5/1980/AD', 'MMMM/d/yyyy/GG', new Date(1980, 1, 5, 0));
+ expectParse('1955/February/5/BC', 'yyyy/MMMM/d/GG', new Date(1955, 1, 5, 0));
+ expectParse('11-08-13 AD', 'd-MM-yy GG', new Date(2013, 7, 11, 0));
+ expectParse('0001/03/6 BC', 'yyyy/MM/d GG', oldDate);
+ });
+
+ it('should work correctly for `GGG`', function() {
+ expectParse('17.November.13.AD', 'd.MMMM.yy.GGG', new Date(2013, 10, 17, 0));
+ expectParse('8-March-1991-BC', 'd-MMMM-yyyy-GGG', new Date(1991, 2, 8, 0));
+ expectParse('February/5/1980/AD', 'MMMM/d/yyyy/GGG', new Date(1980, 1, 5, 0));
+ expectParse('1955/February/5/BC', 'yyyy/MMMM/d/GGG', new Date(1955, 1, 5, 0));
+ expectParse('11-08-13 AD', 'd-MM-yy GGG', new Date(2013, 7, 11, 0));
+ expectParse('0001/03/6 BC', 'yyyy/MM/d GGG', oldDate);
+ });
+
+ it('should work correctly for `GGGG`', function() {
+ expectParse('17.November.13.Anno Domini', 'd.MMMM.yy.GGGG', new Date(2013, 10, 17, 0));
+ expectParse('8-March-1991-Before Christ', 'd-MMMM-yyyy-GGGG', new Date(1991, 2, 8, 0));
+ expectParse('February/5/1980/Anno Domini', 'MMMM/d/yyyy/GGGG', new Date(1980, 1, 5, 0));
+ expectParse('1955/February/5/Before Christ', 'yyyy/MMMM/d/GGGG', new Date(1955, 1, 5, 0));
+ expectParse('11-08-13 Anno Domini', 'd-MM-yy GGGG', new Date(2013, 7, 11, 0));
+ expectParse('0001/03/6 Before Christ', 'yyyy/MM/d GGGG', oldDate);
+ });
});
describe('with predefined formats', function() {
@@ -145,13 +574,59 @@ describe('date parser', function () {
});
});
+ describe('with value literals', function() {
+ describe('filter', function() {
+ it('should work with multiple literals', function() {
+ expect(dateParser.filter(new Date(2013, 0, 29), 'd \'de\' MMMM \'de\' y')).toEqual('29 de January de 2013');
+ });
+
+ it('should work with escaped single quote', function() {
+ expect(dateParser.filter(new Date(2015, 2, 22, 12), 'd.MMMM.yy h \'o\'\'clock\'')).toEqual('22.March.15 12 o\'clock');
+ });
+
+ it('should work with only a single quote', function() {
+ expect(dateParser.filter(new Date(2015, 2, 22), 'd.MMMM.yy \'\'\'')).toEqual('22.March.15 \'');
+ });
+
+ it('should work with trailing literal', function() {
+ expect(dateParser.filter(new Date(2013, 0, 1), '\'year\' y')).toEqual('year 2013');
+ });
+
+ it('should work without whitespace', function() {
+ expect(dateParser.filter(new Date(2013, 0, 1), '\'year:\'y')).toEqual('year:2013');
+ });
+ });
+
+ describe('parse', function() {
+ it('should work with multiple literals', function() {
+ expect(dateParser.parse('29 de January de 2013', 'd \'de\' MMMM \'de\' y')).toEqual(new Date(2013, 0, 29));
+ });
+
+ it('should work with escaped single quote', function() {
+ expect(dateParser.parse('22.March.15 12 o\'clock', 'd.MMMM.yy h \'o\'\'clock\'')).toEqual(new Date(2015, 2, 22, 12));
+ });
+
+ it('should work with only a single quote', function() {
+ expect(dateParser.parse('22.March.15 \'', 'd.MMMM.yy \'\'\'')).toEqual(new Date(2015, 2, 22));
+ });
+
+ it('should work with trailing literal', function() {
+ expect(dateParser.parse('year 2013', '\'year\' y')).toEqual(new Date(2013, 0, 1));
+ });
+
+ it('should work without whitespace', function() {
+ expect(dateParser.parse('year:2013', '\'year:\'y')).toEqual(new Date(2013, 0, 1));
+ });
+ });
+ });
+
describe('with edge case', function() {
it('should not work for invalid number of days in February', function() {
- expect(dateParser.parse('29.02.2013', 'dd.MM.yyyy')).toBeUndefined();
+ expectParse('29.02.2013', 'dd.MM.yyyy', undefined);
});
it('should not work for 0 number of days', function() {
- expect(dateParser.parse('00.02.2013', 'dd.MM.yyyy')).toBeUndefined();
+ expectParse('00.02.2013', 'dd.MM.yyyy', undefined);
});
it('should work for 29 days in February for leap years', function() {
@@ -159,18 +634,185 @@ describe('date parser', function () {
});
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();
+ expectParse('31-04-2013', 'dd-MM-yyyy', undefined);
+ expectParse('November 31, 2013', 'MMMM d, yyyy', undefined);
+ });
+ });
+
+ describe('base date', function() {
+ var baseDate;
+
+ beforeEach(function() {
+ baseDate = new Date(2010, 10, 10);
});
+
+ it('should pre-initialize our date with a base date', function() {
+ expect(expectBaseParse('2015', 'yyyy', baseDate, new Date(2015, 10, 10)));
+ expect(expectBaseParse('1', 'M', baseDate, new Date(2010, 0, 10)));
+ expect(expectBaseParse('1', 'd', baseDate, new Date(2010, 10, 1)));
+ });
+
+ it('should ignore the base date when it is an invalid date', inject(function($log) {
+ spyOn($log, 'warn');
+ expect(expectBaseParse('30-12', 'dd-MM', new Date('foo'), new Date(1900, 11, 30)));
+ expect(expectBaseParse('30-2015', 'dd-yyyy', 'I am a cat', new Date(2015, 0, 30)));
+ expect($log.warn).toHaveBeenCalledWith('dateparser:', 'baseDate is not a valid date');
+ }));
});
it('should not parse non-string inputs', function() {
- expect(dateParser.parse(123456, 'dd.MM.yyyy')).toBe(123456);
+ expectParse(123456, 'dd.MM.yyyy', 123456);
var date = new Date();
- expect(dateParser.parse(date, 'dd.MM.yyyy')).toBe(date);
+ expectParse(date, 'dd.MM.yyyy', date);
});
it('should not parse if no format is specified', function() {
- expect(dateParser.parse('21.08.1951', '')).toBe('21.08.1951');
+ expectParse('21.08.1951', '', '21.08.1951');
+ });
+
+ it('should reinitialize when locale changes', inject(function($locale) {
+ spyOn(dateParser, 'init').and.callThrough();
+ expect($locale.id).toBe('en-us');
+
+ $locale.id = 'en-uk';
+
+ dateParser.parse('22.March.15.22', 'd.MMMM.yy.s');
+
+ expect(dateParser.init).toHaveBeenCalled();
+ }));
+
+ describe('timezone functions', function() {
+ describe('toTimezone', function() {
+ it('adjusts date: PST - EST', function() {
+ var date = new Date('2008-01-01T00:00:00.000Z');
+ var toWestDate = dateParser.toTimezone(date, 'PST');
+ var toEastDate = dateParser.toTimezone(date, 'EST');
+ expect(toWestDate.getTime() - toEastDate.getTime()).toEqual(1000 * 60 * 60 * 3);
+ });
+
+ it('adjusts date: GMT-0500 - GMT+0500', function() {
+ var date = new Date('2008-01-01T00:00:00.000Z');
+ var toWestDate = dateParser.toTimezone(date, 'GMT-0500');
+ var toEastDate = dateParser.toTimezone(date, 'GMT+0500');
+ expect(toWestDate.getTime() - toEastDate.getTime()).toEqual(1000 * 60 * 60 * 10);
+ });
+
+ it('adjusts date: -600 - +600', function() {
+ var date = new Date('2008-01-01T00:00:00.000Z');
+ var toWestDate = dateParser.toTimezone(date, '-600');
+ var toEastDate = dateParser.toTimezone(date, '+600');
+ expect(toWestDate.getTime() - toEastDate.getTime()).toEqual(1000 * 60 * 60 * 12);
+ });
+
+ it('tolerates null date', function() {
+ var date = null;
+ var toNullDate = dateParser.toTimezone(date, '-600');
+ expect(toNullDate).toEqual(date);
+ });
+
+ it('tolerates null timezone', function() {
+ var date = new Date('2008-01-01T00:00:00.000Z');
+ var toNullTimezoneDate = dateParser.toTimezone(date, null);
+ expect(toNullTimezoneDate).toEqual(date);
+ });
+ });
+
+ describe('fromTimezone', function() {
+ it('adjusts date: PST - EST', function() {
+ var date = new Date('2008-01-01T00:00:00.000Z');
+ var fromWestDate = dateParser.fromTimezone(date, 'PST');
+ var fromEastDate = dateParser.fromTimezone(date, 'EST');
+ expect(fromWestDate.getTime() - fromEastDate.getTime()).toEqual(1000 * 60 * 60 * -3);
+ });
+
+ it('adjusts date: GMT-0500 - GMT+0500', function() {
+ var date = new Date('2008-01-01T00:00:00.000Z');
+ var fromWestDate = dateParser.fromTimezone(date, 'GMT-0500');
+ var fromEastDate = dateParser.fromTimezone(date, 'GMT+0500');
+ expect(fromWestDate.getTime() - fromEastDate.getTime()).toEqual(1000 * 60 * 60 * -10);
+ });
+
+ it('adjusts date: -600 - +600', function() {
+ var date = new Date('2008-01-01T00:00:00.000Z');
+ var fromWestDate = dateParser.fromTimezone(date, '-600');
+ var fromEastDate = dateParser.fromTimezone(date, '+600');
+ expect(fromWestDate.getTime() - fromEastDate.getTime()).toEqual(1000 * 60 * 60 * -12);
+ });
+
+ it('tolerates null date', function() {
+ var date = null;
+ var toNullDate = dateParser.fromTimezone(date, '-600');
+ expect(toNullDate).toEqual(date);
+ });
+
+ it('tolerates null timezone', function() {
+ var date = new Date('2008-01-01T00:00:00.000Z');
+ var toNullTimezoneDate = dateParser.fromTimezone(date, null);
+ expect(toNullTimezoneDate).toEqual(date);
+ });
+ });
+
+ describe('timezoneToOffset', function() {
+ it('calculates minutes off from current timezone', function() {
+ var offsetMinutesUtc = dateParser.timezoneToOffset('UTC');
+ var offsetMinutesUtcPlus1 = dateParser.timezoneToOffset('GMT+0100');
+ expect(offsetMinutesUtc - offsetMinutesUtcPlus1).toEqual(60);
+ });
+ });
+
+ describe('addDateMinutes', function() {
+ it('adds minutes to a date', function() {
+ var date = new Date('2008-01-01T00:00:00.000Z');
+ var oneHourMore = dateParser.addDateMinutes(date, 60);
+ expect(oneHourMore).toEqual(new Date('2008-01-01T01:00:00.000Z'));
+ });
+ });
+
+ describe('convertTimezoneToLocal', function() {
+ it('adjusts date: PST - EST', function() {
+ var date = new Date('2008-01-01T00:00:00.000Z');
+ var toWestDate = dateParser.convertTimezoneToLocal(date, 'PST');
+ var toEastDate = dateParser.convertTimezoneToLocal(date, 'EST');
+ expect(toWestDate.getTime() - toEastDate.getTime()).toEqual(1000 * 60 * 60 * 3);
+ });
+
+ it('adjusts date: GMT-0500 - GMT+0500', function() {
+ var date = new Date('2008-01-01T00:00:00.000Z');
+ var toWestDate = dateParser.convertTimezoneToLocal(date, 'GMT-0500');
+ var toEastDate = dateParser.convertTimezoneToLocal(date, 'GMT+0500');
+ expect(toWestDate.getTime() - toEastDate.getTime()).toEqual(1000 * 60 * 60 * 10);
+ });
+
+ it('adjusts date: -600 - +600', function() {
+ var date = new Date('2008-01-01T00:00:00.000Z');
+ var toWestDate = dateParser.convertTimezoneToLocal(date, '-600');
+ var toEastDate = dateParser.convertTimezoneToLocal(date, '+600');
+ expect(toWestDate.getTime() - toEastDate.getTime()).toEqual(1000 * 60 * 60 * 12);
+ });
+ });
+ });
+
+ describe('overrideParser', function() {
+ var twoDigitYearParser = function (value) {
+ this.year = +value + (+value > 30 ? 1900 : 2000);
+ };
+
+ it('should get the current parser', function() {
+ expect(dateParser.getParser('yy')).toBeTruthy();
+ });
+
+ it('should override the parser', function() {
+ dateParser.overrideParser('yy', twoDigitYearParser);
+ expect(dateParser.parse('68', 'yy').getFullYear()).toEqual(1968);
+ expect(dateParser.parse('67', 'yy').getFullYear()).toEqual(1967);
+ expect(dateParser.parse('31', 'yy').getFullYear()).toEqual(1931);
+ expect(dateParser.parse('30', 'yy').getFullYear()).toEqual(2030);
+ });
+
+ it('should clear cached parsers', function() {
+ expect(dateParser.parse('68', 'yy').getFullYear()).toEqual(2068);
+ dateParser.overrideParser('yy', twoDigitYearParser);
+ expect(dateParser.parse('68', 'yy').getFullYear()).toEqual(1968);
+ });
});
});
diff --git a/src/datepicker/datepicker.css b/src/datepicker/datepicker.css
new file mode 100644
index 0000000000..c49e180968
--- /dev/null
+++ b/src/datepicker/datepicker.css
@@ -0,0 +1,11 @@
+.uib-datepicker .uib-title {
+ width: 100%;
+}
+
+.uib-day button, .uib-month button, .uib-year button {
+ min-width: 100%;
+}
+
+.uib-left, .uib-right {
+ width: 100%
+}
diff --git a/src/datepicker/datepicker.js b/src/datepicker/datepicker.js
index a98b48dc7e..fd4e263660 100644
--- a/src/datepicker/datepicker.js
+++ b/src/datepicker/datepicker.js
@@ -1,62 +1,154 @@
-angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootstrap.position'])
+angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootstrap.isClass'])
-.constant('datepickerConfig', {
+.value('$datepickerSuppressError', false)
+
+.value('$datepickerLiteralWarning', true)
+
+.constant('uibDatepickerConfig', {
+ datepickerMode: 'day',
formatDay: 'dd',
formatMonth: 'MMMM',
formatYear: 'yyyy',
formatDayHeader: 'EEE',
formatDayTitle: 'MMMM yyyy',
formatMonthTitle: 'yyyy',
- datepickerMode: 'day',
- minMode: 'day',
+ maxDate: null,
maxMode: 'year',
- showWeeks: true,
- startingDay: 0,
- yearRange: 20,
minDate: null,
- maxDate: null,
- shortcutPropagation: false
+ minMode: 'day',
+ monthColumns: 3,
+ ngModelOptions: {},
+ shortcutPropagation: false,
+ showWeeks: true,
+ yearColumns: 5,
+ yearRows: 4
})
-.controller('DatepickerController', ['$scope', '$attrs', '$parse', '$interpolate', '$timeout', '$log', 'dateFilter', 'datepickerConfig', function($scope, $attrs, $parse, $interpolate, $timeout, $log, dateFilter, datepickerConfig) {
+.controller('UibDatepickerController', ['$scope', '$element', '$attrs', '$parse', '$interpolate', '$locale', '$log', 'dateFilter', 'uibDatepickerConfig', '$datepickerLiteralWarning', '$datepickerSuppressError', 'uibDateParser',
+ function($scope, $element, $attrs, $parse, $interpolate, $locale, $log, dateFilter, datepickerConfig, $datepickerLiteralWarning, $datepickerSuppressError, dateParser) {
var self = this,
- ngModelCtrl = { $setViewValue: angular.noop }; // nullModelCtrl;
+ ngModelCtrl = { $setViewValue: angular.noop }, // nullModelCtrl;
+ ngModelOptions = {},
+ watchListeners = [];
+
+ $element.addClass('uib-datepicker');
+ $attrs.$set('role', 'application');
+
+ if (!$scope.datepickerOptions) {
+ $scope.datepickerOptions = {};
+ }
// Modes chain
this.modes = ['day', 'month', 'year'];
- // Configuration attributes
- angular.forEach(['formatDay', 'formatMonth', 'formatYear', 'formatDayHeader', 'formatDayTitle', 'formatMonthTitle',
- 'minMode', 'maxMode', 'showWeeks', 'startingDay', 'yearRange', 'shortcutPropagation'], function( key, index ) {
- self[key] = angular.isDefined($attrs[key]) ? (index < 8 ? $interpolate($attrs[key])($scope.$parent) : $scope.$parent.$eval($attrs[key])) : datepickerConfig[key];
- });
+ [
+ 'customClass',
+ 'dateDisabled',
+ 'datepickerMode',
+ 'formatDay',
+ 'formatDayHeader',
+ 'formatDayTitle',
+ 'formatMonth',
+ 'formatMonthTitle',
+ 'formatYear',
+ 'maxDate',
+ 'maxMode',
+ 'minDate',
+ 'minMode',
+ 'monthColumns',
+ 'showWeeks',
+ 'shortcutPropagation',
+ 'startingDay',
+ 'yearColumns',
+ 'yearRows'
+ ].forEach(function(key) {
+ switch (key) {
+ case 'customClass':
+ case 'dateDisabled':
+ $scope[key] = $scope.datepickerOptions[key] || angular.noop;
+ break;
+ case 'datepickerMode':
+ $scope.datepickerMode = angular.isDefined($scope.datepickerOptions.datepickerMode) ?
+ $scope.datepickerOptions.datepickerMode : datepickerConfig.datepickerMode;
+ break;
+ case 'formatDay':
+ case 'formatDayHeader':
+ case 'formatDayTitle':
+ case 'formatMonth':
+ case 'formatMonthTitle':
+ case 'formatYear':
+ self[key] = angular.isDefined($scope.datepickerOptions[key]) ?
+ $interpolate($scope.datepickerOptions[key])($scope.$parent) :
+ datepickerConfig[key];
+ break;
+ case 'monthColumns':
+ case 'showWeeks':
+ case 'shortcutPropagation':
+ case 'yearColumns':
+ case 'yearRows':
+ self[key] = angular.isDefined($scope.datepickerOptions[key]) ?
+ $scope.datepickerOptions[key] : datepickerConfig[key];
+ break;
+ case 'startingDay':
+ if (angular.isDefined($scope.datepickerOptions.startingDay)) {
+ self.startingDay = $scope.datepickerOptions.startingDay;
+ } else if (angular.isNumber(datepickerConfig.startingDay)) {
+ self.startingDay = datepickerConfig.startingDay;
+ } else {
+ self.startingDay = ($locale.DATETIME_FORMATS.FIRSTDAYOFWEEK + 8) % 7;
+ }
- // 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;
+ break;
+ case 'maxDate':
+ case 'minDate':
+ $scope.$watch('datepickerOptions.' + key, function(value) {
+ if (value) {
+ if (angular.isDate(value)) {
+ self[key] = dateParser.fromTimezone(new Date(value), ngModelOptions.getOption('timezone'));
+ } else {
+ if ($datepickerLiteralWarning) {
+ $log.warn('Literal date support has been deprecated, please switch to date object usage');
+ }
+
+ self[key] = new Date(dateFilter(value, 'medium'));
+ }
+ } else {
+ self[key] = datepickerConfig[key] ?
+ dateParser.fromTimezone(new Date(datepickerConfig[key]), ngModelOptions.getOption('timezone')) :
+ null;
+ }
+
+ self.refreshView();
+ });
+
+ break;
+ case 'maxMode':
+ case 'minMode':
+ if ($scope.datepickerOptions[key]) {
+ $scope.$watch(function() { return $scope.datepickerOptions[key]; }, function(value) {
+ self[key] = $scope[key] = angular.isDefined(value) ? value : $scope.datepickerOptions[key];
+ if (key === 'minMode' && self.modes.indexOf($scope.datepickerOptions.datepickerMode) < self.modes.indexOf(self[key]) ||
+ key === 'maxMode' && self.modes.indexOf($scope.datepickerOptions.datepickerMode) > self.modes.indexOf(self[key])) {
+ $scope.datepickerMode = self[key];
+ $scope.datepickerOptions.datepickerMode = self[key];
+ }
+ });
+ } else {
+ self[key] = $scope[key] = datepickerConfig[key] || null;
+ }
+
+ break;
}
});
- $scope.datepickerMode = $scope.datepickerMode || datepickerConfig.datepickerMode;
- $scope.maxMode = self.maxMode;
$scope.uniqueId = 'datepicker-' + $scope.$id + '-' + Math.floor(Math.random() * 10000);
- if(angular.isDefined($attrs.initDate)) {
- this.activeDate = $scope.$parent.$eval($attrs.initDate) || new Date();
- $scope.$parent.$watch($attrs.initDate, function(initDate){
- if(initDate && (ngModelCtrl.$isEmpty(ngModelCtrl.$modelValue) || ngModelCtrl.$invalid)){
- self.activeDate = initDate;
- self.refreshView();
- }
- });
- } else {
- this.activeDate = new Date();
+ $scope.disabled = angular.isDefined($attrs.disabled) || false;
+ if (angular.isDefined($attrs.ngDisabled)) {
+ watchListeners.push($scope.$parent.$watch($attrs.ngDisabled, function(disabled) {
+ $scope.disabled = disabled;
+ self.refreshView();
+ }));
}
$scope.isActive = function(dateObject) {
@@ -67,8 +159,26 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
return false;
};
- this.init = function( ngModelCtrl_ ) {
+ this.init = function(ngModelCtrl_) {
ngModelCtrl = ngModelCtrl_;
+ ngModelOptions = extractOptions(ngModelCtrl);
+
+ if ($scope.datepickerOptions.initDate) {
+ self.activeDate = dateParser.fromTimezone($scope.datepickerOptions.initDate, ngModelOptions.getOption('timezone')) || new Date();
+ $scope.$watch('datepickerOptions.initDate', function(initDate) {
+ if (initDate && (ngModelCtrl.$isEmpty(ngModelCtrl.$modelValue) || ngModelCtrl.$invalid)) {
+ self.activeDate = dateParser.fromTimezone(initDate, ngModelOptions.getOption('timezone'));
+ self.refreshView();
+ }
+ });
+ } else {
+ self.activeDate = new Date();
+ }
+
+ var date = ngModelCtrl.$modelValue ? new Date(ngModelCtrl.$modelValue) : new Date();
+ this.activeDate = !isNaN(date) ?
+ dateParser.fromTimezone(date, ngModelOptions.getOption('timezone')) :
+ dateParser.fromTimezone(new Date(), ngModelOptions.getOption('timezone'));
ngModelCtrl.$render = function() {
self.render();
@@ -76,48 +186,72 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
};
this.render = function() {
- if ( ngModelCtrl.$viewValue ) {
- var date = new Date( ngModelCtrl.$viewValue ),
+ if (ngModelCtrl.$viewValue) {
+ var date = new Date(ngModelCtrl.$viewValue),
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.');
+ if (isValid) {
+ this.activeDate = dateParser.fromTimezone(date, ngModelOptions.getOption('timezone'));
+ } else if (!$datepickerSuppressError) {
+ $log.error('Datepicker directive: "ng-model" value must be a Date object');
}
- ngModelCtrl.$setValidity('date', isValid);
}
this.refreshView();
};
this.refreshView = function() {
- if ( this.element ) {
+ if (this.element) {
+ $scope.selectedDt = null;
this._refreshView();
+ if ($scope.activeDt) {
+ $scope.activeDateId = $scope.activeDt.uid;
+ }
var date = ngModelCtrl.$viewValue ? new Date(ngModelCtrl.$viewValue) : null;
- ngModelCtrl.$setValidity('date-disabled', !date || (this.element && !this.isDisabled(date)));
+ date = dateParser.fromTimezone(date, ngModelOptions.getOption('timezone'));
+ ngModelCtrl.$setValidity('dateDisabled', !date ||
+ this.element && !this.isDisabled(date));
}
};
this.createDateObject = function(date, format) {
var model = ngModelCtrl.$viewValue ? new Date(ngModelCtrl.$viewValue) : null;
- return {
+ model = dateParser.fromTimezone(model, ngModelOptions.getOption('timezone'));
+ var today = new Date();
+ today = dateParser.fromTimezone(today, ngModelOptions.getOption('timezone'));
+ var time = this.compare(date, today);
+ var dt = {
date: date,
- label: dateFilter(date, format),
+ label: dateParser.filter(date, format),
selected: model && this.compare(date, model) === 0,
disabled: this.isDisabled(date),
- current: this.compare(date, new Date()) === 0,
- customClass: this.customClass(date)
+ past: time < 0,
+ current: time === 0,
+ future: time > 0,
+ customClass: this.customClass(date) || null
};
+
+ if (model && this.compare(date, model) === 0) {
+ $scope.selectedDt = dt;
+ }
+
+ if (self.activeDate && this.compare(dt.date, self.activeDate) === 0) {
+ $scope.activeDt = dt;
+ }
+
+ return dt;
};
- this.isDisabled = function( date ) {
- return ((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})));
+ this.isDisabled = function(date) {
+ return $scope.disabled ||
+ this.minDate && this.compare(date, this.minDate) < 0 ||
+ this.maxDate && this.compare(date, this.maxDate) > 0 ||
+ $scope.dateDisabled && $scope.dateDisabled({date: date, mode: $scope.datepickerMode});
};
- this.customClass = function( date ) {
- return $scope.customClass({date: date, mode: $scope.datepickerMode});
- };
+ this.customClass = function(date) {
+ return $scope.customClass({date: date, mode: $scope.datepickerMode});
+ };
// Split array into smaller arrays
this.split = function(arr, size) {
@@ -128,616 +262,424 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
return arrays;
};
- $scope.select = function( date ) {
- if ( $scope.datepickerMode === self.minMode ) {
- var dt = ngModelCtrl.$viewValue ? new Date( ngModelCtrl.$viewValue ) : new Date(0, 0, 0, 0, 0, 0, 0);
- dt.setFullYear( date.getFullYear(), date.getMonth(), date.getDate() );
- ngModelCtrl.$setViewValue( dt );
+ $scope.select = function(date) {
+ if ($scope.datepickerMode === self.minMode) {
+ var dt = ngModelCtrl.$viewValue ? dateParser.fromTimezone(new Date(ngModelCtrl.$viewValue), ngModelOptions.getOption('timezone')) : new Date(0, 0, 0, 0, 0, 0, 0);
+ dt.setFullYear(date.getFullYear(), date.getMonth(), date.getDate());
+ dt = dateParser.toTimezone(dt, ngModelOptions.getOption('timezone'));
+ ngModelCtrl.$setViewValue(dt);
ngModelCtrl.$render();
} else {
self.activeDate = date;
- $scope.datepickerMode = self.modes[ self.modes.indexOf( $scope.datepickerMode ) - 1 ];
+ setMode(self.modes[self.modes.indexOf($scope.datepickerMode) - 1]);
+
+ $scope.$emit('uib:datepicker.mode');
}
+
+ $scope.$broadcast('uib:datepicker.focus');
};
- $scope.move = function( direction ) {
+ $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 ) {
+ $scope.toggleMode = function(direction) {
direction = direction || 1;
- if (($scope.datepickerMode === self.maxMode && direction === 1) || ($scope.datepickerMode === self.minMode && 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 ];
+ setMode(self.modes[self.modes.indexOf($scope.datepickerMode) + direction]);
+
+ $scope.$emit('uib:datepicker.mode');
};
// Key event mapper
- $scope.keys = { 13:'enter', 32:'space', 33:'pageup', 34:'pagedown', 35:'end', 36:'home', 37:'left', 38:'up', 39:'right', 40:'down' };
+ $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);
+ self.element[0].focus();
};
// Listen for focus requests from popup directive
- $scope.$on('datepicker.focus', focusElement);
+ $scope.$on('uib:datepicker.focus', focusElement);
- $scope.keydown = function( evt ) {
+ $scope.keydown = function(evt) {
var key = $scope.keys[evt.which];
- if ( !key || evt.shiftKey || evt.altKey ) {
+ if (!key || evt.shiftKey || evt.altKey || $scope.disabled) {
return;
}
evt.preventDefault();
- if(!self.shortcutPropagation){
- evt.stopPropagation();
+ if (!self.shortcutPropagation) {
+ evt.stopPropagation();
}
if (key === 'enter' || key === 'space') {
- if ( self.isDisabled(self.activeDate)) {
+ 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: '&',
- customClass: '&',
- shortcutPropagation: '&?'
- },
- require: ['datepicker', '?^ngModel'],
- controller: 'DatepickerController',
- link: function(scope, element, attrs, ctrls) {
- var datepickerCtrl = ctrls[0], ngModelCtrl = ctrls[1];
+ $element.on('keydown', function(evt) {
+ $scope.$apply(function() {
+ $scope.keydown(evt);
+ });
+ });
- if ( ngModelCtrl ) {
- datepickerCtrl.init( ngModelCtrl );
- }
+ $scope.$on('$destroy', function() {
+ //Clear all watch listeners on destroy
+ while (watchListeners.length) {
+ watchListeners.shift()();
}
- };
-})
-
-.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 thursdayIndex = (4 + 7 - ctrl.startingDay) % 7,
- numWeeks = scope.rows.length;
- for (var curWeek = 0; curWeek < numWeeks; curWeek++) {
- scope.weekNumbers.push(
- getISO8601WeekNumber( scope.rows[curWeek][thursdayIndex].date ));
- }
- }
- };
+ function setMode(mode) {
+ $scope.datepickerMode = mode;
+ $scope.datepickerOptions.datepickerMode = mode;
+ }
- ctrl.compare = function(date1, date2) {
- return (new Date( date1.getFullYear(), date1.getMonth(), date1.getDate() ) - new Date( date2.getFullYear(), date2.getMonth(), date2.getDate() ) );
- };
+ function extractOptions(ngModelCtrl) {
+ var ngModelOptions;
- 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;
- }
+ if (angular.version.minor < 6) { // in angular < 1.6 $options could be missing
+ // guarantee a value
+ ngModelOptions = ngModelCtrl.$options ||
+ $scope.datepickerOptions.ngModelOptions ||
+ datepickerConfig.ngModelOptions ||
+ {};
- 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);
+ // mimic 1.6+ api
+ ngModelOptions.getOption = function (key) {
+ return ngModelOptions[key];
};
-
- ctrl.refreshView();
+ } else { // in angular >=1.6 $options is always present
+ // ng-model-options defaults timezone to null; don't let its precedence squash a non-null value
+ var timezone = ngModelCtrl.$options.getOption('timezone') ||
+ ($scope.datepickerOptions.ngModelOptions ? $scope.datepickerOptions.ngModelOptions.timezone : null) ||
+ (datepickerConfig.ngModelOptions ? datepickerConfig.ngModelOptions.timezone : null);
+
+ // values passed to createChild override existing values
+ ngModelOptions = ngModelCtrl.$options // start with a ModelOptions instance
+ .createChild(datepickerConfig.ngModelOptions) // lowest precedence
+ .createChild($scope.datepickerOptions.ngModelOptions)
+ .createChild(ngModelCtrl.$options) // highest precedence
+ .createChild({timezone: timezone}); // to keep from squashing a non-null value
}
- };
-}])
-.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
- });
- }
+ return ngModelOptions;
+ }
+}])
- scope.title = dateFilter(ctrl.activeDate, ctrl.formatMonthTitle);
- scope.rows = ctrl.split(months, 3);
- };
+.controller('UibDaypickerController', ['$scope', '$element', 'dateFilter', function(scope, $element, dateFilter) {
+ var DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
- ctrl.compare = function(date1, date2) {
- return new Date( date1.getFullYear(), date1.getMonth() ) - new Date( date2.getFullYear(), date2.getMonth() );
- };
+ this.step = { months: 1 };
+ this.element = $element;
+ function getDaysInMonth(year, month) {
+ return month === 1 && year % 4 === 0 &&
+ (year % 100 !== 0 || year % 400 === 0) ? 29 : DAYS_IN_MONTH[month];
+ }
- 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);
- };
+ this.init = function(ctrl) {
+ angular.extend(ctrl, this);
+ scope.showWeeks = ctrl.showWeeks;
+ ctrl.refreshView();
+ };
- ctrl.refreshView();
+ this.getDates = function(startDate, n) {
+ var dates = new Array(n), current = new Date(startDate), i = 0, date;
+ while (i < n) {
+ date = new Date(current);
+ dates[i++] = date;
+ current.setDate(current.getDate() + 1);
}
+ return dates;
};
-}])
-.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;
- }
+ this._refreshView = function() {
+ var year = this.activeDate.getFullYear(),
+ month = this.activeDate.getMonth(),
+ firstDayOfMonth = new Date(this.activeDate);
- ctrl._refreshView = function() {
- var years = new Array(range);
+ firstDayOfMonth.setFullYear(year, month, 1);
- 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
- });
- }
+ var difference = this.startingDay - firstDayOfMonth.getDay(),
+ numDisplayedFromPreviousMonth = difference > 0 ?
+ 7 - difference : - difference,
+ firstDate = new Date(firstDayOfMonth);
- scope.title = [years[0].label, years[range - 1].label].join(' - ');
- scope.rows = ctrl.split(years, 5);
- };
+ if (numDisplayedFromPreviousMonth > 0) {
+ firstDate.setDate(-numDisplayedFromPreviousMonth + 1);
+ }
- ctrl.compare = function(date1, date2) {
- return date1.getFullYear() - date2.getFullYear();
- };
+ // 42 is the number of days on a six-week calendar
+ var days = this.getDates(firstDate, 42);
+ for (var i = 0; i < 42; i ++) {
+ days[i] = angular.extend(this.createDateObject(days[i], this.formatDay), {
+ secondary: days[i].getMonth() !== month,
+ uid: scope.uniqueId + '-' + i
+ });
+ }
- 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);
+ scope.labels = new Array(7);
+ for (var j = 0; j < 7; j++) {
+ scope.labels[j] = {
+ abbr: dateFilter(days[j].date, this.formatDayHeader),
+ full: dateFilter(days[j].date, 'EEEE')
};
-
- ctrl.refreshView();
}
- };
-}])
-
-.constant('datepickerPopupConfig', {
- datepickerPopup: 'yyyy-MM-dd',
- html5Types: {
- date: 'yyyy-MM-dd',
- 'datetime-local': 'yyyy-MM-ddTHH:mm:ss.sss',
- 'month': 'yyyy-MM'
- },
- 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: '&',
- customClass: '&'
- },
- 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'];
- };
+ scope.title = dateFilter(this.activeDate, this.formatDayTitle);
+ scope.rows = this.split(days, 7);
- var isHtml5DateInput = false;
- if (datepickerPopupConfig.html5Types[attrs.type]) {
- dateFormat = datepickerPopupConfig.html5Types[attrs.type];
- isHtml5DateInput = true;
- } else {
- dateFormat = attrs.datepickerPopup || datepickerPopupConfig.datepickerPopup;
- attrs.$observe('datepickerPopup', function(value, oldValue) {
- var newDateFormat = value || datepickerPopupConfig.datepickerPopup;
- // Invalidate the $modelValue to ensure that formatters re-run
- // FIXME: Refactor when PR is merged: https://github.com/angular/angular.js/pull/10764
- if (newDateFormat !== dateFormat) {
- dateFormat = newDateFormat;
- ngModel.$modelValue = null;
-
- if (!dateFormat) {
- throw new Error('datepickerPopup must have a date format specified.');
- }
- }
- });
+ if (scope.showWeeks) {
+ scope.weekNumbers = [];
+ var thursdayIndex = (4 + 7 - this.startingDay) % 7,
+ numWeeks = scope.rows.length;
+ for (var curWeek = 0; curWeek < numWeeks; curWeek++) {
+ scope.weekNumbers.push(
+ getISO8601WeekNumber(scope.rows[curWeek][thursdayIndex].date));
}
+ }
+ };
- if (!dateFormat) {
- throw new Error('datepickerPopup must have a date format specified.');
- }
+ this.compare = function(date1, date2) {
+ var _date1 = new Date(date1.getFullYear(), date1.getMonth(), date1.getDate());
+ var _date2 = new Date(date2.getFullYear(), date2.getMonth(), date2.getDate());
+ _date1.setFullYear(date1.getFullYear());
+ _date2.setFullYear(date2.getFullYear());
+ return _date1 - _date2;
+ };
- if (isHtml5DateInput && attrs.datepickerPopup) {
- throw new Error('HTML5 date input types do not support custom formats.');
- }
+ 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;
+ }
- // popup element used to display calendar
- var popupEl = angular.element('
');
- popupEl.attr({
- 'ng-model': 'date',
- 'ng-change': 'dateSelection()'
- });
+ this.handleKeyDown = function(key, evt) {
+ var date = this.activeDate.getDate();
+
+ if (key === 'left') {
+ date = date - 1;
+ } else if (key === 'up') {
+ date = date - 7;
+ } else if (key === 'right') {
+ date = date + 1;
+ } else if (key === 'down') {
+ date = date + 7;
+ } else if (key === 'pageup' || key === 'pagedown') {
+ var month = this.activeDate.getMonth() + (key === 'pageup' ? - 1 : 1);
+ this.activeDate.setMonth(month, 1);
+ date = Math.min(getDaysInMonth(this.activeDate.getFullYear(), this.activeDate.getMonth()), date);
+ } else if (key === 'home') {
+ date = 1;
+ } else if (key === 'end') {
+ date = getDaysInMonth(this.activeDate.getFullYear(), this.activeDate.getMonth());
+ }
+ this.activeDate.setDate(date);
+ };
+}])
- function cameltoDash( string ){
- return string.replace(/([A-Z])/g, function($1) { return '-' + $1.toLowerCase(); });
- }
+.controller('UibMonthpickerController', ['$scope', '$element', 'dateFilter', function(scope, $element, dateFilter) {
+ this.step = { years: 1 };
+ this.element = $element;
- // datepicker element
- var datepickerEl = angular.element(popupEl.children()[0]);
- if (isHtml5DateInput) {
- if (attrs.type == 'month') {
- datepickerEl.attr('datepicker-mode', '"month"');
- datepickerEl.attr('min-mode', 'month');
- }
- }
+ this.init = function(ctrl) {
+ angular.extend(ctrl, this);
+ ctrl.refreshView();
+ };
- if ( attrs.datepickerOptions ) {
- var options = scope.$parent.$eval(attrs.datepickerOptions);
- if(options.initDate) {
- scope.initDate = options.initDate;
- datepickerEl.attr( 'init-date', 'initDate' );
- delete options.initDate;
- }
- angular.forEach(options, function( value, option ) {
- datepickerEl.attr( cameltoDash(option), value );
- });
- }
+ this._refreshView = function() {
+ var months = new Array(12),
+ year = this.activeDate.getFullYear(),
+ date;
- scope.watchData = {};
- angular.forEach(['minDate', 'maxDate', 'datepickerMode', 'initDate', 'shortcutPropagation'], 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 ( angular.isFunction(setAttribute) && value !== oldvalue ) {
- setAttribute(scope.$parent, value);
- }
- });
- }
- }
+ for (var i = 0; i < 12; i++) {
+ date = new Date(this.activeDate);
+ date.setFullYear(year, i, 1);
+ months[i] = angular.extend(this.createDateObject(date, this.formatMonth), {
+ uid: scope.uniqueId + '-' + i
});
- if (attrs.dateDisabled) {
- datepickerEl.attr('date-disabled', 'dateDisabled({ date: date, mode: mode })');
- }
-
- if (attrs.showWeeks) {
- datepickerEl.attr('show-weeks', attrs.showWeeks);
- }
+ }
- if (attrs.customClass){
- datepickerEl.attr('custom-class', 'customClass({ date: date, mode: mode })');
- }
+ scope.title = dateFilter(this.activeDate, this.formatMonthTitle);
+ scope.rows = this.split(months, this.monthColumns);
+ scope.yearHeaderColspan = this.monthColumns > 3 ? this.monthColumns - 2 : 1;
+ };
- function parseDate(viewValue) {
- if (angular.isNumber(viewValue)) {
- // presumably timestamp to date object
- viewValue = new Date(viewValue);
- }
+ this.compare = function(date1, date2) {
+ var _date1 = new Date(date1.getFullYear(), date1.getMonth());
+ var _date2 = new Date(date2.getFullYear(), date2.getMonth());
+ _date1.setFullYear(date1.getFullYear());
+ _date2.setFullYear(date2.getFullYear());
+ return _date1 - _date2;
+ };
- if (!viewValue) {
- return null;
- } else if (angular.isDate(viewValue) && !isNaN(viewValue)) {
- return viewValue;
- } else if (angular.isString(viewValue)) {
- var date = dateParser.parse(viewValue, dateFormat, scope.date) || new Date(viewValue);
- if (isNaN(date)) {
- return undefined;
- } else {
- return date;
- }
- } else {
- return undefined;
- }
- }
+ this.handleKeyDown = function(key, evt) {
+ var date = this.activeDate.getMonth();
+
+ if (key === 'left') {
+ date = date - 1;
+ } else if (key === 'up') {
+ date = date - this.monthColumns;
+ } else if (key === 'right') {
+ date = date + 1;
+ } else if (key === 'down') {
+ date = date + this.monthColumns;
+ } else if (key === 'pageup' || key === 'pagedown') {
+ var year = this.activeDate.getFullYear() + (key === 'pageup' ? - 1 : 1);
+ this.activeDate.setFullYear(year);
+ } else if (key === 'home') {
+ date = 0;
+ } else if (key === 'end') {
+ date = 11;
+ }
+ this.activeDate.setMonth(date);
+ };
+}])
- function validator(modelValue, viewValue) {
- var value = modelValue || viewValue;
- if (angular.isNumber(value)) {
- value = new Date(value);
- }
- if (!value) {
- return true;
- } else if (angular.isDate(value) && !isNaN(value)) {
- return true;
- } else if (angular.isString(value)) {
- var date = dateParser.parse(value, dateFormat) || new Date(value);
- return !isNaN(date);
- } else {
- return false;
- }
- }
+.controller('UibYearpickerController', ['$scope', '$element', 'dateFilter', function(scope, $element, dateFilter) {
+ var columns, range;
+ this.element = $element;
- if (!isHtml5DateInput) {
- // Internal API to maintain the correct ng-invalid-[key] class
- ngModel.$$parserName = 'date';
- ngModel.$validators.date = validator;
- ngModel.$parsers.unshift(parseDate);
- ngModel.$formatters.push(function (value) {
- scope.date = value;
- return ngModel.$isEmpty(value) ? value : dateFilter(value, dateFormat);
- });
- }
- else {
- ngModel.$formatters.push(function (value) {
- scope.date = value;
- return value;
- });
- }
+ function getStartingYear(year) {
+ return parseInt((year - 1) / range, 10) * range + 1;
+ }
- // Inner change
- scope.dateSelection = function(dt) {
- if (angular.isDefined(dt)) {
- scope.date = dt;
- }
- var date = scope.date ? dateFilter(scope.date, dateFormat) : '';
- element.val(date);
- ngModel.$setViewValue(date);
+ this.yearpickerInit = function() {
+ columns = this.yearColumns;
+ range = this.yearRows * columns;
+ this.step = { years: range };
+ };
- if ( closeOnDateSelection ) {
- scope.isOpen = false;
- element[0].focus();
- }
- };
+ this._refreshView = function() {
+ var years = new Array(range), date;
- // Detect changes in the view from the text box
- ngModel.$viewChangeListeners.push(function () {
- scope.date = dateParser.parse(ngModel.$viewValue, dateFormat, scope.date) || new Date(ngModel.$viewValue);
+ for (var i = 0, start = getStartingYear(this.activeDate.getFullYear()); i < range; i++) {
+ date = new Date(this.activeDate);
+ date.setFullYear(start + i, 0, 1);
+ years[i] = angular.extend(this.createDateObject(date, this.formatYear), {
+ uid: scope.uniqueId + '-' + i
});
+ }
- 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.title = [years[0].label, years[range - 1].label].join(' - ');
+ scope.rows = this.split(years, columns);
+ scope.columns = columns;
+ };
- scope.keydown = function(evt) {
- if (evt.which === 27) {
- evt.preventDefault();
- if (scope.isOpen) {
- evt.stopPropagation();
- }
- scope.close();
- } else if (evt.which === 40 && !scope.isOpen) {
- scope.isOpen = true;
- }
- };
+ this.compare = function(date1, date2) {
+ return date1.getFullYear() - date2.getFullYear();
+ };
- 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');
+ this.handleKeyDown = function(key, evt) {
+ var date = this.activeDate.getFullYear();
+
+ if (key === 'left') {
+ date = date - 1;
+ } else if (key === 'up') {
+ date = date - columns;
+ } else if (key === 'right') {
+ date = date + 1;
+ } else if (key === 'down') {
+ date = date + columns;
+ } else if (key === 'pageup' || key === 'pagedown') {
+ date += (key === 'pageup' ? - 1 : 1) * range;
+ } else if (key === 'home') {
+ date = getStartingYear(this.activeDate.getFullYear());
+ } else if (key === 'end') {
+ date = getStartingYear(this.activeDate.getFullYear()) + range - 1;
+ }
+ this.activeDate.setFullYear(date);
+ };
+}])
- $document.bind('click', documentClickBind);
- } else {
- $document.unbind('click', documentClickBind);
- }
- });
+.directive('uibDatepicker', function() {
+ return {
+ templateUrl: function(element, attrs) {
+ return attrs.templateUrl || 'uib/template/datepicker/datepicker.html';
+ },
+ scope: {
+ datepickerOptions: '=?'
+ },
+ require: ['uibDatepicker', '^ngModel'],
+ restrict: 'A',
+ controller: 'UibDatepickerController',
+ controllerAs: 'datepicker',
+ link: function(scope, element, attrs, ctrls) {
+ var datepickerCtrl = ctrls[0], ngModelCtrl = ctrls[1];
- scope.select = function( date ) {
- if (date === 'today') {
- var today = new Date();
- if (angular.isDate(scope.date)) {
- date = new Date(scope.date);
- date.setFullYear(today.getFullYear(), today.getMonth(), today.getDate());
- } else {
- date = new Date(today.setHours(0, 0, 0, 0));
- }
- }
- scope.dateSelection( date );
- };
+ datepickerCtrl.init(ngModelCtrl);
+ }
+ };
+})
- scope.close = function() {
- scope.isOpen = false;
- element[0].focus();
- };
+.directive('uibDaypicker', function() {
+ return {
+ templateUrl: function(element, attrs) {
+ return attrs.templateUrl || 'uib/template/datepicker/day.html';
+ },
+ require: ['^uibDatepicker', 'uibDaypicker'],
+ restrict: 'A',
+ controller: 'UibDaypickerController',
+ link: function(scope, element, attrs, ctrls) {
+ var datepickerCtrl = ctrls[0],
+ daypickerCtrl = ctrls[1];
- var $popup = $compile(popupEl)(scope);
- // Prevent jQuery cache memory leak (template is now redundant after linking)
- popupEl.remove();
+ daypickerCtrl.init(datepickerCtrl);
+ }
+ };
+})
- if ( appendToBody ) {
- $document.find('body').append($popup);
- } else {
- element.after($popup);
- }
+.directive('uibMonthpicker', function() {
+ return {
+ templateUrl: function(element, attrs) {
+ return attrs.templateUrl || 'uib/template/datepicker/month.html';
+ },
+ require: ['^uibDatepicker', 'uibMonthpicker'],
+ restrict: 'A',
+ controller: 'UibMonthpickerController',
+ link: function(scope, element, attrs, ctrls) {
+ var datepickerCtrl = ctrls[0],
+ monthpickerCtrl = ctrls[1];
- scope.$on('$destroy', function() {
- $popup.remove();
- element.unbind('keydown', keydown);
- $document.unbind('click', documentClickBind);
- });
+ monthpickerCtrl.init(datepickerCtrl);
}
};
-}])
+})
-.directive('datepickerPopupWrap', function() {
+.directive('uibYearpicker', 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();
- });
+ templateUrl: function(element, attrs) {
+ return attrs.templateUrl || 'uib/template/datepicker/year.html';
+ },
+ require: ['^uibDatepicker', 'uibYearpicker'],
+ restrict: 'A',
+ controller: 'UibYearpickerController',
+ link: function(scope, element, attrs, ctrls) {
+ var ctrl = ctrls[0];
+ angular.extend(ctrl, ctrls[1]);
+ ctrl.yearpickerInit();
+
+ ctrl.refreshView();
}
};
});
diff --git a/src/datepicker/docs/demo.html b/src/datepicker/docs/demo.html
index 7ea3b98fd2..e083965698 100644
--- a/src/datepicker/docs/demo.html
+++ b/src/datepicker/docs/demo.html
@@ -15,38 +15,12 @@
Inline
-
-
-
-
Popup
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
Today
- 2009-08-24
+ 2009-08-24Clear
- Min date
-
\ No newline at end of file
+ Min date
+
diff --git a/src/datepicker/docs/demo.js b/src/datepicker/docs/demo.js
index 1412d392c4..2473d9b885 100644
--- a/src/datepicker/docs/demo.js
+++ b/src/datepicker/docs/demo.js
@@ -4,56 +4,55 @@ angular.module('ui.bootstrap.demo').controller('DatepickerDemoCtrl', function ($
};
$scope.today();
- $scope.clear = function () {
+ $scope.clear = function() {
$scope.dt = null;
};
- // Disable weekend selection
- $scope.disabled = function(date, mode) {
- return ( mode === 'day' && ( date.getDay() === 0 || date.getDay() === 6 ) );
+ $scope.options = {
+ customClass: getDayClass,
+ minDate: new Date(),
+ showWeeks: true
};
+ // Disable weekend selection
+ function disabled(data) {
+ var date = data.date,
+ mode = data.mode;
+ return mode === 'day' && (date.getDay() === 0 || date.getDay() === 6);
+ }
+
$scope.toggleMin = function() {
- $scope.minDate = $scope.minDate ? null : new Date();
+ $scope.options.minDate = $scope.options.minDate ? null : new Date();
};
- $scope.toggleMin();
-
- $scope.open = function($event) {
- $event.preventDefault();
- $event.stopPropagation();
- $scope.opened = true;
- };
+ $scope.toggleMin();
- $scope.dateOptions = {
- formatYear: 'yy',
- startingDay: 1
+ $scope.setDate = function(year, month, day) {
+ $scope.dt = new Date(year, month, day);
};
- $scope.formats = ['dd-MMMM-yyyy', 'yyyy/MM/dd', 'dd.MM.yyyy', 'shortDate'];
- $scope.format = $scope.formats[0];
-
var tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
- var afterTomorrow = new Date();
- afterTomorrow.setDate(tomorrow.getDate() + 2);
- $scope.events =
- [
- {
- date: tomorrow,
- status: 'full'
- },
- {
- date: afterTomorrow,
- status: 'partially'
- }
- ];
+ var afterTomorrow = new Date(tomorrow);
+ afterTomorrow.setDate(tomorrow.getDate() + 1);
+ $scope.events = [
+ {
+ date: tomorrow,
+ status: 'full'
+ },
+ {
+ date: afterTomorrow,
+ status: 'partially'
+ }
+ ];
- $scope.getDayClass = function(date, mode) {
+ function getDayClass(data) {
+ var date = data.date,
+ mode = data.mode;
if (mode === 'day') {
var dayToCheck = new Date(date).setHours(0,0,0,0);
- for (var i=0;i<$scope.events.length;i++){
+ for (var i = 0; i < $scope.events.length; i++) {
var currentDay = new Date($scope.events[i].date).setHours(0,0,0,0);
if (dayToCheck === currentDay) {
@@ -63,5 +62,5 @@ angular.module('ui.bootstrap.demo').controller('DatepickerDemoCtrl', function ($
}
return '';
- };
+ }
});
diff --git a/src/datepicker/docs/readme.md b/src/datepicker/docs/readme.md
index baa493d234..d1928e272d 100644
--- a/src/datepicker/docs/readme.md
+++ b/src/datepicker/docs/readme.md
@@ -1,127 +1,147 @@
-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`
- :
- The date object.
-
- * `datepicker-mode`
- _(Defaults: 'day')_ :
- Current mode of the datepicker _(day|month|year)_. Can be used to initialize datepicker to specific mode.
-
- * `min-date`
- _(Default: null)_ :
- Defines the minimum available date.
-
- * `max-date`
- _(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)_.
-
- * `custom-class (date, mode)`
- _(Default: null)_ :
- An optional expression to add classes 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 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.
-
- * `shortcut-propagation`
- _(Default: false)_ :
- An option to disable or enable shortcut's event propagation.
-
-
-### 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.
+Our datepicker is flexible and fully customizable.
+
+You can navigate through days, months and years.
+
+The datepicker has 3 modes:
+
+* `day` - In this mode you're presented with a 6-week calendar for a specified month.
+* `month` - In this mode you can select a month within a selected year.
+* `year` - In this mode you are presented with a range of years (20 by default).
+
+### uib-datepicker settings
+
+* `ng-model`
+ $
+ -
+ The date object. Must be a Javascript `Date` object. You may use the `uibDateParser` service to assist in string-to-object conversion.
+
+* `ng-model-options`
+ $
+ C
+ _(Default: `{}`)_ -
+ Supported [angular ngModelOptions](https://docs.angularjs.org/api/ng/directive/ngModelOptions):
+ * allowInvalid
+ * timezone
+
+* `template-url`
+ _(Default: `uib/template/datepicker/datepicker.html`)_ -
+ Add the ability to override the template used on the component.
+
+Apart from the previous settings, to configure the uib-datepicker you need to create an object in Javascript with all the options and use it on the `datepicker-options` attribute:
+
+* `datepicker-options`
+ $ -
+ An object to configure the datepicker in one place.
+
+ * `customClass ({date: date, mode: mode})` -
+ An optional expression to add classes based on passing an object with date and current mode properties.
+
+ * `dateDisabled ({date: date, mode: mode})` -
+ An optional expression to disable visible options based on passing an object with date and current mode properties.
+
+ * `datepickerMode`
+ C
+
+ _(Default: `day`)_ -
+ Current mode of the datepicker _(day|month|year)_. Can be used to initialize the datepicker in a specific mode.
+
+ * `formatDay`
+ C
+ _(Default: `dd`)_ -
+ Format of day in month.
+
+ * `formatMonth`
+ C
+ _(Default: `MMMM`)_ -
+ Format of month in year.
+
+ * `formatYear`
+ C
+ _(Default: `yyyy`)_ -
+ Format of year in year range.
+
+ * `formatDayHeader`
+ C
+ _(Default: `EEE`)_ -
+ Format of day in week header.
+
+ * `formatDayTitle`
+ C
+ _(Default: `MMMM yyyy`)_ -
+ Format of title when selecting day.
+
+ * `formatMonthTitle`
+ C
+ _(Default: `yyyy`)_ -
+ Format of title when selecting month.
+
+ * `initDate`
+
+ _(Default: `null`)_ -
+ The initial date view when no model value is specified.
+
+ * `maxDate`
+ C
+
+ _(Default: `null`)_ -
+ Defines the maximum available date. Requires a Javascript Date object.
+
+ * `maxMode`
+ C
+
+ _(Default: `year`)_ -
+ Sets an upper limit for mode.
+
+ * `minDate`
+ C
+
+ _(Default: `null`)_ -
+ Defines the minimum available date. Requires a Javascript Date object.
+
+ * `minMode`
+ C
+
+ _(Default: `day`)_ -
+ Sets a lower limit for mode.
+
+ * `monthColumns`
+ C
+ _(Default: `3`)_ -
+ Number of columns displayed in month selection.
+
+ * `ngModelOptions`
+ C
+ _(Default: `null`)_ -
+ Sets `ngModelOptions` for datepicker. This can be overridden through attribute usage
+
+ * `shortcutPropagation`
+ C
+ _(Default: `false`)_ -
+ An option to disable the propagation of the keydown event.
+
+ * `showWeeks`
+ C
+ _(Default: `true`)_ -
+ Whether to display week numbers.
+
+ * `startingDay`
+ C
+ *(Default: `$locale.DATETIME_FORMATS.FIRSTDAYOFWEEK`)* -
+ Starting day of the week from 0-6 (0=Sunday, ..., 6=Saturday).
+
+ * `yearRows`
+ C
+ _(Default: `4`)_ -
+ Number of rows displayed in year selection.
+
+ * `yearColumns`
+ C
+ _(Default: `5`)_ -
+ Number of columns displayed in year selection.
+
+### Keyboard support
+
+Depending on datepicker's current mode, the date may refer either to day, month or year. Accordingly, the term view refers either to a month, year or year range.
* `Left`: Move focus to the previous date. Will move to the last date of the previous view, if the current date is the first date of a view.
* `Right`: Move focus to the next date. Will move to the first date of the following view, if the current date is the last date of a view.
@@ -134,4 +154,8 @@ Depending on datepicker's current mode, the date may reffer either to day, month
* `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.
\ No newline at end of file
+ * `Esc`: Will close popup, and move focus to the input.
+
+**Notes**
+
+If the date a user enters falls outside of the min-/max-date range, a `dateDisabled` validation error will show on the form.
diff --git a/src/datepicker/index-nocss.js b/src/datepicker/index-nocss.js
new file mode 100644
index 0000000000..95b22e2644
--- /dev/null
+++ b/src/datepicker/index-nocss.js
@@ -0,0 +1,13 @@
+require('../dateparser');
+require('../isClass');
+require('../../template/datepicker/datepicker.html.js');
+require('../../template/datepicker/day.html.js');
+require('../../template/datepicker/month.html.js');
+require('../../template/datepicker/year.html.js');
+require('./datepicker');
+
+var MODULE_NAME = 'ui.bootstrap.module.datepicker';
+
+angular.module(MODULE_NAME, ['ui.bootstrap.datepicker', 'uib/template/datepicker/datepicker.html', 'uib/template/datepicker/day.html', 'uib/template/datepicker/month.html', 'uib/template/datepicker/year.html']);
+
+module.exports = MODULE_NAME;
diff --git a/src/datepicker/index.js b/src/datepicker/index.js
new file mode 100644
index 0000000000..f7ff1cfba1
--- /dev/null
+++ b/src/datepicker/index.js
@@ -0,0 +1,2 @@
+require('./datepicker.css');
+module.exports = require('./index-nocss.js');
diff --git a/src/datepicker/test/datepicker.spec.js b/src/datepicker/test/datepicker.spec.js
index 09aa37e26b..8dd924c8d8 100644
--- a/src/datepicker/test/datepicker.spec.js
+++ b/src/datepicker/test/datepicker.spec.js
@@ -1,11 +1,10 @@
-describe('datepicker directive', function () {
- var $rootScope, $compile, element;
+describe('datepicker', function() {
+ var $rootScope, $compile, $templateCache, element;
beforeEach(module('ui.bootstrap.datepicker'));
- beforeEach(module('template/datepicker/datepicker.html'));
- beforeEach(module('template/datepicker/day.html'));
- beforeEach(module('template/datepicker/month.html'));
- beforeEach(module('template/datepicker/year.html'));
- beforeEach(module('template/datepicker/popup.html'));
+ beforeEach(module('uib/template/datepicker/datepicker.html'));
+ beforeEach(module('uib/template/datepicker/day.html'));
+ beforeEach(module('uib/template/datepicker/month.html'));
+ beforeEach(module('uib/template/datepicker/year.html'));
beforeEach(module(function($compileProvider) {
$compileProvider.directive('dateModel', function() {
return {
@@ -26,14 +25,13 @@ describe('datepicker directive', function () {
};
});
}));
- beforeEach(inject(function(_$compile_, _$rootScope_) {
- $compile = _$compile_;
- $rootScope = _$rootScope_;
- $rootScope.date = new Date('September 30, 2010 15:30:00');
- }));
+
+ function getTitleCell() {
+ return element.find('th').eq(1);
+ }
function getTitleButton() {
- return element.find('th').eq(1).find('button').first();
+ return getTitleCell().find('button').first();
}
function getTitle() {
@@ -63,7 +61,7 @@ describe('datepicker directive', function () {
var els = getLabelsRow().find('th'),
labels = [];
for (var i = dayMode ? 1 : 0, n = els.length; i < n; i++) {
- labels.push( els.eq(i).text() );
+ labels.push(els.eq(i).text());
}
return labels;
}
@@ -72,7 +70,7 @@ describe('datepicker directive', function () {
var rows = element.find('tbody').find('tr'),
weeks = [];
for (var i = 0, n = rows.length; i < n; i++) {
- weeks.push( rows.eq(i).find('td').eq(0).first().text() );
+ weeks.push(rows.eq(i).find('td').eq(0).first().text());
}
return weeks;
}
@@ -84,7 +82,7 @@ describe('datepicker directive', function () {
for (var j = 0, numRows = tr.length; j < numRows; j++) {
var cols = tr.eq(j).find('td'), days = [];
for (var i = dayMode ? 1 : 0, n = cols.length; i < n; i++) {
- days.push( cols.eq(i).find('button').text() );
+ days.push(cols.eq(i).find('button').text());
}
rows.push(days);
}
@@ -110,11 +108,19 @@ describe('datepicker directive', function () {
function expectSelectedElement(index) {
var buttons = getAllOptionsEl();
- angular.forEach( buttons, function( button, idx ) {
- expect(angular.element(button).hasClass('btn-info')).toBe( idx === index );
+ angular.forEach( buttons, function(button, idx) {
+ expect(angular.element(button).hasClass('btn-info')).toBe(idx === index);
});
}
+ function getSelectedElement(index) {
+ var buttons = getAllOptionsEl();
+ var el = $.grep(buttons, function(button, idx) {
+ return angular.element(button).hasClass('btn-info');
+ })[0];
+ return angular.element(el);
+ }
+
function triggerKeyDown(element, key, ctrl) {
var keyCodes = {
'enter': 13,
@@ -137,2050 +143,1613 @@ describe('datepicker directive', function () {
element.trigger(e);
}
- describe('', function () {
- beforeEach(function() {
- element = $compile('')($rootScope);
- $rootScope.$digest();
- });
+ describe('$datepickerLiteralWarning', function() {
+ var $compile,
+ $log,
+ $scope;
- it('is has a `
` element', function() {
- expect(element.find('table').length).toBe(1);
- });
+ it('should warn when using literals for min date by default', function() {
+ inject(function(_$log_, _$rootScope_, _$compile_) {
+ $log = _$log_;
+ $scope = _$rootScope_.$new();
+ $compile = _$compile_;
+ });
- it('shows the correct title', function() {
- expect(getTitle()).toBe('September 2010');
- });
+ spyOn($log, 'warn');
+ $scope.options = {
+ minDate: '1984-01-01'
+ };
+ element = $compile('')($scope);
+ $scope.$digest();
- it('shows the label row & the correct day labels', function() {
- expect(getLabelsRow().css('display')).not.toBe('none');
- expect(getLabels(true)).toEqual(['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']);
+ expect($log.warn).toHaveBeenCalledWith('Literal date support has been deprecated, please switch to date object usage');
});
- it('renders the calendar days correctly', function() {
- expect(getOptions(true)).toEqual([
- ['29', '30', '31', '01', '02', '03', '04'],
- ['05', '06', '07', '08', '09', '10', '11'],
- ['12', '13', '14', '15', '16', '17', '18'],
- ['19', '20', '21', '22', '23', '24', '25'],
- ['26', '27', '28', '29', '30', '01', '02'],
- ['03', '04', '05', '06', '07', '08', '09']
- ]);
- });
+ it('should suppress warning when using literals for min date', function() {
+ module(function($provide) {
+ $provide.value('$datepickerLiteralWarning', false);
+ });
+ inject(function(_$log_, _$rootScope_, _$compile_) {
+ $log = _$log_;
+ $scope = _$rootScope_.$new();
+ $compile = _$compile_;
+ });
- it('renders the week numbers based on ISO 8601', function() {
- expect(getWeeks()).toEqual(['35', '36', '37', '38', '39', '40']);
- });
+ spyOn($log, 'warn');
+ $scope.options = {
+ minDate: '1984-01-01'
+ };
+ element = $compile('')($scope);
+ $scope.$digest();
- it('value is correct', function() {
- expect($rootScope.date).toEqual(new Date('September 30, 2010 15:30:00'));
+ expect($log.warn).not.toHaveBeenCalled();
});
- it('has `selected` only the correct day', function() {
- expectSelectedElement( 32 );
- });
+ it('should warn when using literals for max date by default', function() {
+ inject(function(_$log_, _$rootScope_, _$compile_) {
+ $log = _$log_;
+ $scope = _$rootScope_.$new();
+ $compile = _$compile_;
+ });
- it('has no `selected` day when model is cleared', function() {
- $rootScope.date = null;
- $rootScope.$digest();
+ spyOn($log, 'warn');
+ $scope.options = {
+ maxDate: '1984-01-01'
+ };
+ element = $compile('')($scope);
+ $scope.$digest();
- expect($rootScope.date).toBe(null);
- expectSelectedElement( null );
+ expect($log.warn).toHaveBeenCalledWith('Literal date support has been deprecated, please switch to date object usage');
});
- it('does not change current view when model is cleared', function() {
- $rootScope.date = null;
- $rootScope.$digest();
+ it('should suppress warning when using literals for max date', function() {
+ module(function($provide) {
+ $provide.value('$datepickerLiteralWarning', false);
+ });
+ inject(function(_$log_, _$rootScope_, _$compile_) {
+ $log = _$log_;
+ $scope = _$rootScope_.$new();
+ $compile = _$compile_;
+ });
- expect($rootScope.date).toBe(null);
- expect(getTitle()).toBe('September 2010');
+ spyOn($log, 'warn');
+ $scope.options = {
+ maxDate: '1984-01-01'
+ };
+ element = $compile('')($scope);
+ $scope.$digest();
+
+ expect($log.warn).not.toHaveBeenCalled();
});
+ });
+
+ describe('$datepickerSuppressError', function() {
+ var $compile,
+ $log,
+ $scope;
- it('`disables` visible dates from other months', function() {
- var buttons = getAllOptionsEl();
- angular.forEach(buttons, function( button, index ) {
- expect(angular.element(button).find('span').hasClass('text-muted')).toBe( index < 3 || index > 32 );
+ it('should not suppress log error message for ng-model date error by default', function() {
+ inject(function(_$log_, _$rootScope_, _$compile_) {
+ $log = _$log_;
+ $scope = _$rootScope_.$new();
+ $compile = _$compile_;
});
- });
- it('updates the model when a day is clicked', function() {
- clickOption( 17 );
- expect($rootScope.date).toEqual(new Date('September 15, 2010 15:30:00'));
- });
+ spyOn($log, 'error');
+ element = $compile('')($scope);
- it('moves to the previous month & renders correctly when `previous` button is clicked', function() {
- clickPreviousButton();
-
- expect(getTitle()).toBe('August 2010');
- expect(getLabels(true)).toEqual(['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']);
- expect(getOptions(true)).toEqual([
- ['01', '02', '03', '04', '05', '06', '07'],
- ['08', '09', '10', '11', '12', '13', '14'],
- ['15', '16', '17', '18', '19', '20', '21'],
- ['22', '23', '24', '25', '26', '27', '28'],
- ['29', '30', '31', '01', '02', '03', '04'],
- ['05', '06', '07', '08', '09', '10', '11']
- ]);
-
- expectSelectedElement( null, null );
+ $scope.locals = {
+ date: 'lalala'
+ };
+ $scope.$digest();
+ expect($log.error).toHaveBeenCalled();
});
- it('updates the model only when a day is clicked in the `previous` month', function() {
- clickPreviousButton();
- expect($rootScope.date).toEqual(new Date('September 30, 2010 15:30:00'));
+ it('should not suppress log error message for ng-model date error when false', function() {
+ module(function($provide) {
+ $provide.value('$datepickerSuppressError', false);
+ });
+
+ inject(function(_$log_, _$rootScope_, _$compile_) {
+ $log = _$log_;
+ $scope = _$rootScope_.$new();
+ $compile = _$compile_;
+ });
- clickOption( 17 );
- expect($rootScope.date).toEqual(new Date('August 18, 2010 15:30:00'));
- });
+ spyOn($log, 'error');
+ element = $compile('')($scope);
- it('moves to the next month & renders correctly when `next` button is clicked', function() {
- clickNextButton();
-
- expect(getTitle()).toBe('October 2010');
- expect(getLabels(true)).toEqual(['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']);
- expect(getOptions(true)).toEqual([
- ['26', '27', '28', '29', '30', '01', '02'],
- ['03', '04', '05', '06', '07', '08', '09'],
- ['10', '11', '12', '13', '14', '15', '16'],
- ['17', '18', '19', '20', '21', '22', '23'],
- ['24', '25', '26', '27', '28', '29', '30'],
- ['31', '01', '02', '03', '04', '05', '06']
- ]);
-
- expectSelectedElement( 4 );
+ $scope.locals = {
+ date: 'lalala'
+ };
+ $scope.$digest();
+ expect($log.error).toHaveBeenCalled();
});
- it('updates the model only when a day is clicked in the `next` month', function() {
- clickNextButton();
- expect($rootScope.date).toEqual(new Date('September 30, 2010 15:30:00'));
+ it('should suppress log error message for ng-model date error when true', function() {
+ module(function($provide) {
+ $provide.value('$datepickerSuppressError', true);
+ });
- clickOption( 17 );
- expect($rootScope.date).toEqual(new Date('October 13, 2010 15:30:00'));
- });
+ inject(function(_$log_, _$rootScope_, _$compile_) {
+ $log = _$log_;
+ $scope = _$rootScope_.$new();
+ $compile = _$compile_;
+ });
+ spyOn($log, 'error');
- it('updates the calendar when a day of another month is selected', function() {
- clickOption( 33 );
- expect($rootScope.date).toEqual(new Date('October 01, 2010 15:30:00'));
- expect(getTitle()).toBe('October 2010');
- expect(getLabels(true)).toEqual(['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']);
- expect(getOptions(true)).toEqual([
- ['26', '27', '28', '29', '30', '01', '02'],
- ['03', '04', '05', '06', '07', '08', '09'],
- ['10', '11', '12', '13', '14', '15', '16'],
- ['17', '18', '19', '20', '21', '22', '23'],
- ['24', '25', '26', '27', '28', '29', '30'],
- ['31', '01', '02', '03', '04', '05', '06']
- ]);
-
- expectSelectedElement( 5 );
- });
+ element = $compile('')($scope);
- // issue #1697
- it('should not "jump" months', function() {
- $rootScope.date = new Date('January 30, 2014');
- $rootScope.$digest();
- clickNextButton();
- expect(getTitle()).toBe('February 2014');
- clickPreviousButton();
- expect(getTitle()).toBe('January 2014');
+ $scope.locals = {
+ date: 'lalala'
+ };
+ $scope.$digest();
+ expect($log.error).not.toHaveBeenCalled();
});
+ });
- describe('when `model` changes', function () {
- function testCalendar() {
- expect(getTitle()).toBe('November 2005');
- expect(getOptions(true)).toEqual([
- ['30', '31', '01', '02', '03', '04', '05'],
- ['06', '07', '08', '09', '10', '11', '12'],
- ['13', '14', '15', '16', '17', '18', '19'],
- ['20', '21', '22', '23', '24', '25', '26'],
- ['27', '28', '29', '30', '01', '02', '03'],
- ['04', '05', '06', '07', '08', '09', '10']
- ]);
+ describe('', function() {
+ beforeEach(inject(function(_$compile_, _$rootScope_, _$templateCache_) {
+ $compile = _$compile_;
+ $rootScope = _$rootScope_;
+ $rootScope.date = new Date('September 30, 2010 15:30:00');
+ $templateCache = _$templateCache_;
+ }));
- expectSelectedElement( 8 );
- }
+ describe('with no initial date', function() {
+ beforeEach(function() {
+ jasmine.clock().install();
+ });
- describe('to a Date object', function() {
- it('updates', function() {
- $rootScope.date = new Date('November 7, 2005 23:30:00');
- $rootScope.$digest();
- testCalendar();
- expect(angular.isDate($rootScope.date)).toBe(true);
- });
+ afterEach(function() {
+ jasmine.clock().uninstall();
+ });
- it('to a date that is invalid, it gets invalid', function() {
- $rootScope.date = new Date('pizza');
- $rootScope.$digest();
- expect(element.hasClass('ng-invalid')).toBeTruthy();
- expect(element.hasClass('ng-invalid-date')).toBeTruthy();
- expect(angular.isDate($rootScope.date)).toBe(true);
- expect(isNaN($rootScope.date)).toBe(true);
- });
+ it('should have an active date equal to the current date', function() {
+ var baseTime = new Date(2015, 2, 23);
+ jasmine.clock().mockDate(baseTime);
+
+ element = $compile('baz');
- it('to a Number, it updates calendar', function() {
- $rootScope.date = parseInt((new Date('November 7, 2005 23:30:00')).getTime(), 10);
- $rootScope.$digest();
- testCalendar();
- expect(angular.isNumber($rootScope.date)).toBe(true);
- });
+ element = $compile('')($rootScope);
+ $rootScope.$digest();
- it('to a string that can be parsed by Date, it updates calendar', function() {
- $rootScope.date = 'November 7, 2005 23:30:00';
- $rootScope.$digest();
- testCalendar();
- expect(angular.isString($rootScope.date)).toBe(true);
- });
+ expect(element.html()).toBe('
baz
');
+ });
- it('to a string that cannot be parsed by Date, it gets invalid', function() {
- $rootScope.date = 'pizza';
- $rootScope.$digest();
- expect(element.hasClass('ng-invalid')).toBeTruthy();
- expect(element.hasClass('ng-invalid-date')).toBeTruthy();
- expect($rootScope.date).toBe('pizza');
- });
- });
+ it('should support custom day, month and year templates', function() {
+ $templateCache.put('foo/day.html', '
day
');
+ $templateCache.put('foo/month.html', '
month
');
+ $templateCache.put('foo/year.html', '
year
');
+
+ $templateCache.put('foo/bar.html', '
' +
+ '' +
+ '' +
+ '' +
+ '
');
+
+ element = $compile('')($rootScope);
+ $rootScope.$digest();
+
+ var expectedHtml = '
day
month
year
';
+
+ expect(element.html()).toBe(expectedHtml);
});
- it('does not loop between after max mode', function() {
- expect(getTitle()).toBe('September 2010');
+ it('should expose the controller in the template', function() {
+ $templateCache.put('uib/template/datepicker/datepicker.html', '
')($rootScope);
+ $rootScope.$digest();
+ assignElements(wrapElement);
+ }));
+
+ it('to display the correct value in input', function() {
+ expect(inputEl.val()).toBe('30-September-2010');
+ });
+
+ it('updates the input when a day is clicked', function() {
+ clickOption(17);
+ expect(inputEl.val()).toBe('15-September-2010');
+ expect($rootScope.date).toEqual(new Date('September 15, 2010 15:30:00'));
+ });
+
+ it('updates the input correctly when model changes', function() {
+ $rootScope.date = new Date('January 10, 1983 10:00:00');
+ $rootScope.$digest();
+ expect(inputEl.val()).toBe('10-January-1983');
+ });
+ });
+
+ describe('custom format with time', function() {
+ beforeEach(inject(function() {
+ var wrapElement = $compile('
')($rootScope);
+ $rootScope.$digest();
+ assignElements(wrapElement);
+ }));
+
+ it('updates the model correctly when the input value changes', function() {
+ $rootScope.date = new Date(2015, 10, 24, 10, 0);
+ $rootScope.$digest();
+ expect(inputEl.val()).toBe('Nov-24-2015 10:00 AM');
+
+ inputEl.val('Nov-24-2015 11:00 AM').trigger('input');
+ $rootScope.$digest();
+ expect($rootScope.date).toEqual(new Date(2015, 10, 24, 11, 0));
+ });
+ });
+
+ describe('custom format with optional leading zeroes', function() {
+ beforeEach(inject(function() {
+ var wrapElement = $compile('
')($rootScope);
+ $rootScope.$digest();
+ assignElements(wrapElement);
+ }));
+
+ it('to display the correct value in input', function() {
+ expect(inputEl.val()).toBe('30-09-2010');
+ });
+
+ it('updates the input when a day is clicked', function() {
+ clickOption(10);
+ expect(inputEl.val()).toBe('08-09-2010');
+ expect($rootScope.date).toEqual(new Date('September 8, 2010 15:30:00'));
+ });
+
+ it('updates the input correctly when model changes', function() {
+ $rootScope.date = new Date('December 25, 1983 10:00:00');
+ $rootScope.$digest();
+ expect(inputEl.val()).toBe('25-12-1983');
+ });
+ });
+
+ describe('dynamic custom format', function() {
+ beforeEach(inject(function() {
+ $rootScope.format = 'dd-MMMM-yyyy';
+ var wrapElement = $compile('
')($rootScope);
+ $rootScope.$digest();
+ assignElements(wrapElement);
+ }));
+
+ it('to display the correct value in input', function() {
+ expect(inputEl.val()).toBe('30-September-2010');
+ });
+
+ it('updates the input when a day is clicked', function() {
+ clickOption(17);
+ expect(inputEl.val()).toBe('15-September-2010');
+ expect($rootScope.date).toEqual(new Date('September 15, 2010 15:30:00'));
+ });
+
+ it('updates the input correctly when model changes', function() {
+ $rootScope.date = new Date('August 11, 2013 09:09:00');
+ $rootScope.$digest();
+ expect(inputEl.val()).toBe('11-August-2013');
+ });
+
+ it('updates the input correctly when format changes', function() {
+ $rootScope.format = 'dd/MM/yyyy';
+ $rootScope.$digest();
+ expect(inputEl.val()).toBe('30/09/2010');
+ });
+ });
+
+ describe('format errors', function() {
+ var originalConfig = {};
+ beforeEach(inject(function(uibDatepickerPopupConfig) {
+ angular.extend(originalConfig, uibDatepickerPopupConfig);
+ uibDatepickerPopupConfig.datepickerPopup = null;
+ }));
+ afterEach(inject(function(uibDatepickerPopupConfig) {
+ // return it to the original state
+ angular.extend(uibDatepickerPopupConfig, originalConfig);
+ }));
+
+ it('should throw an error if there is no format', function() {
+ expect(function() {
+ $compile('
')($rootScope);
+ }).toThrow(new Error('uibDatepickerPopup must have a date format specified.'));
+ });
+
+ it('should throw an error if the format changes to null without fallback', function() {
+ $rootScope.format = 'dd-MMMM-yyyy';
+ $compile('
')($rootScope);
+ $rootScope.$digest();
+
+ expect(function() {
+ $rootScope.format = null;
+ $rootScope.$digest();
+ }).toThrow(new Error('uibDatepickerPopup must have a date format specified.'));
+ });
+
+ it('should thrown an error on date inputs with custom formats', function() {
+ expect(function() {
+ $compile('
')($rootScope);
+ }).toThrow(new Error('HTML5 date input types do not support custom formats.'));
+ });
+ });
+
+ describe('european format', function() {
+ it('dd.MM.yyyy', function() {
+ var wrapElement = $compile('
')($rootScope);
}
beforeEach(function() {
@@ -40,107 +43,110 @@ describe('dropdownToggle', function() {
});
it('should toggle on `a` click', function() {
- expect(element.hasClass(dropdownConfig.openClass)).toBe(false);
+ expect(element).not.toHaveClass(dropdownConfig.openClass);
clickDropdownToggle();
- expect(element.hasClass(dropdownConfig.openClass)).toBe(true);
+ expect(element).toHaveClass(dropdownConfig.openClass);
clickDropdownToggle();
- expect(element.hasClass(dropdownConfig.openClass)).toBe(false);
+ expect(element).not.toHaveClass(dropdownConfig.openClass);
});
it('should toggle when an option is clicked', function() {
$document.find('body').append(element);
- expect(element.hasClass(dropdownConfig.openClass)).toBe(false);
+ expect(element).not.toHaveClass(dropdownConfig.openClass);
clickDropdownToggle();
- expect(element.hasClass(dropdownConfig.openClass)).toBe(true);
+ expect(element).toHaveClass(dropdownConfig.openClass);
var optionEl = element.find('ul > li').eq(0).find('a').eq(0);
optionEl.click();
- expect(element.hasClass(dropdownConfig.openClass)).toBe(false);
+ expect(element).not.toHaveClass(dropdownConfig.openClass);
});
it('should close on document click', function() {
clickDropdownToggle();
- expect(element.hasClass(dropdownConfig.openClass)).toBe(true);
+ expect(element).toHaveClass(dropdownConfig.openClass);
$document.click();
- expect(element.hasClass(dropdownConfig.openClass)).toBe(false);
+ expect(element).not.toHaveClass(dropdownConfig.openClass);
});
it('should close on escape key & focus toggle element', function() {
+ var dropdownMenu = element.find('[uib-dropdown-menu]');
$document.find('body').append(element);
clickDropdownToggle();
- triggerKeyDown($document, 27);
- expect(element.hasClass(dropdownConfig.openClass)).toBe(false);
- expect(isFocused(element.find('a'))).toBe(true);
+ var event = triggerKeyDown(dropdownMenu, 27);
+ expect(element).not.toHaveClass(dropdownConfig.openClass);
+ expect(element.find('a')).toHaveFocus();
+ expect(event.stopPropagation).toHaveBeenCalled();
});
it('should not close on backspace key', function() {
clickDropdownToggle();
- triggerKeyDown($document, 8);
- expect(element.hasClass(dropdownConfig.openClass)).toBe(true);
+ triggerKeyDown(element, 8);
+ expect(element).toHaveClass(dropdownConfig.openClass);
});
- it('should close on $location change', function() {
+ it('should not close on right click', function() {
clickDropdownToggle();
- expect(element.hasClass(dropdownConfig.openClass)).toBe(true);
- $rootScope.$broadcast('$locationChangeSuccess');
- $rootScope.$apply();
- expect(element.hasClass(dropdownConfig.openClass)).toBe(false);
+ element.find('ul a').trigger({
+ type: 'mousedown',
+ which: 3
+ });
+ expect(element).toHaveClass(dropdownConfig.openClass);
});
it('should only allow one dropdown to be open at once', function() {
var elm1 = dropdown();
var elm2 = dropdown();
- expect(elm1.hasClass(dropdownConfig.openClass)).toBe(false);
- expect(elm2.hasClass(dropdownConfig.openClass)).toBe(false);
+ expect(elm1).not.toHaveClass(dropdownConfig.openClass);
+ expect(elm2).not.toHaveClass(dropdownConfig.openClass);
- clickDropdownToggle( elm1 );
- expect(elm1.hasClass(dropdownConfig.openClass)).toBe(true);
- expect(elm2.hasClass(dropdownConfig.openClass)).toBe(false);
+ clickDropdownToggle(elm1);
+ expect(elm1).toHaveClass(dropdownConfig.openClass);
+ expect(elm2).not.toHaveClass(dropdownConfig.openClass);
- clickDropdownToggle( elm2 );
- expect(elm1.hasClass(dropdownConfig.openClass)).toBe(false);
- expect(elm2.hasClass(dropdownConfig.openClass)).toBe(true);
+ clickDropdownToggle(elm2);
+ expect(elm1).not.toHaveClass(dropdownConfig.openClass);
+ expect(elm2).toHaveClass(dropdownConfig.openClass);
});
it('should not toggle if the element has `disabled` class', function() {
- var elm = $compile('
Hello
')($rootScope);
+ var elm = $compile('
Hello
')($rootScope);
clickDropdownToggle( elm );
- expect(elm.hasClass(dropdownConfig.openClass)).toBe(false);
+ expect(elm).not.toHaveClass(dropdownConfig.openClass);
});
it('should not toggle if the element is disabled', function() {
- var elm = $compile('
Hello
')($rootScope);
+ var elm = $compile('
Hello
')($rootScope);
elm.find('button').click();
- expect(elm.hasClass(dropdownConfig.openClass)).toBe(false);
+ expect(elm).not.toHaveClass(dropdownConfig.openClass);
});
it('should not toggle if the element has `ng-disabled` as true', function() {
$rootScope.isdisabled = true;
- var elm = $compile('
Hello
')($rootScope);
+ var elm = $compile('
Hello
')($rootScope);
$rootScope.$digest();
elm.find('div').click();
- expect(elm.hasClass(dropdownConfig.openClass)).toBe(false);
+ expect(elm).not.toHaveClass(dropdownConfig.openClass);
$rootScope.isdisabled = false;
$rootScope.$digest();
elm.find('div').click();
- expect(elm.hasClass(dropdownConfig.openClass)).toBe(true);
+ expect(elm).toHaveClass(dropdownConfig.openClass);
});
it('should unbind events on scope destroy', function() {
var $scope = $rootScope.$new();
- var elm = $compile('
Hello
')($scope);
+ var elm = $compile('
Hello
')($scope);
$scope.$digest();
var buttonEl = elm.find('button');
buttonEl.click();
- expect(elm.hasClass(dropdownConfig.openClass)).toBe(true);
+ expect(elm).toHaveClass(dropdownConfig.openClass);
buttonEl.click();
- expect(elm.hasClass(dropdownConfig.openClass)).toBe(false);
+ expect(elm).not.toHaveClass(dropdownConfig.openClass);
$scope.$destroy();
buttonEl.click();
- expect(elm.hasClass(dropdownConfig.openClass)).toBe(false);
+ expect(elm).not.toHaveClass(dropdownConfig.openClass);
});
// issue 270
@@ -148,11 +154,11 @@ describe('dropdownToggle', function() {
var checkboxEl = $compile('')($rootScope);
$rootScope.$digest();
- expect(element.hasClass(dropdownConfig.openClass)).toBe(false);
+ expect(element).not.toHaveClass(dropdownConfig.openClass);
expect($rootScope.clicked).toBeFalsy();
clickDropdownToggle();
- expect(element.hasClass(dropdownConfig.openClass)).toBe(true);
+ expect(element).toHaveClass(dropdownConfig.openClass);
expect($rootScope.clicked).toBeFalsy();
checkboxEl.click();
@@ -172,300 +178,769 @@ describe('dropdownToggle', function() {
});
// pr/issue 3274
- it('should not raise $digest:inprog if dismissed during a digest cycle', function () {
+ it('should not raise $digest:inprog if dismissed during a digest cycle', function() {
clickDropdownToggle();
- expect(element.hasClass(dropdownConfig.openClass)).toBe(true);
+ expect(element).toHaveClass(dropdownConfig.openClass);
- $rootScope.$apply(function () {
+ $rootScope.$apply(function() {
$document.click();
});
- expect(element.hasClass(dropdownConfig.openClass)).toBe(false);
+ expect(element).not.toHaveClass(dropdownConfig.openClass);
});
});
-
+
describe('using dropdownMenuTemplate', function() {
function dropdown() {
- $templateCache.put('custom.html', '
Item 1
');
+ $templateCache.put('custom.html', '
Item 1
');
- return $compile('
')($rootScope);
+ return $compile('
')($rootScope);
}
beforeEach(function() {
element = dropdown();
});
-
+
it('should apply custom template for dropdown menu', function() {
element.find('a').click();
- expect(element.find('ul.dropdown-menu').eq(0).find('li').eq(0).text()).toEqual('Item 1');
+ expect(element.find('ul.uib-dropdown-menu').eq(0).find('li').eq(0).text()).toEqual('Item 1');
});
it('should clear ul when dropdown menu is closed', function() {
element.find('a').click();
- expect(element.find('ul.dropdown-menu').eq(0).find('li').eq(0).text()).toEqual('Item 1');
+ expect(element.find('ul.uib-dropdown-menu').eq(0).find('li').eq(0).text()).toEqual('Item 1');
element.find('a').click();
- expect(element.find('ul.dropdown-menu').eq(0).find('li').length).toEqual(0);
+ expect(element.find('ul.uib-dropdown-menu').eq(0).find('li').length).toEqual(0);
});
});
describe('using dropdown-append-to-body', function() {
+ describe('with no value', function() {
+ function dropdown() {
+ return $compile('
+
- Open me!
- Large modal
- Small modal
- Toggle Animation ({{ animationsEnabled }})
-
Selection from a modal: {{ selected }}
-
\ No newline at end of file
+ Open me!
+ Large modal
+ Small modal
+
+ Modal appended to a custom parent
+
+ Toggle Animation ({{ $ctrl.animationsEnabled }})
+ Open a component modal!
+
+ Open multiple modals at once
+
+
Selection from a modal: {{ $ctrl.selected }}
+
+
+
diff --git a/src/modal/docs/demo.js b/src/modal/docs/demo.js
index 4e1823c72f..53b335dad1 100644
--- a/src/modal/docs/demo.js
+++ b/src/modal/docs/demo.js
@@ -1,51 +1,126 @@
-angular.module('ui.bootstrap.demo').controller('ModalDemoCtrl', function ($scope, $modal, $log) {
+angular.module('ui.bootstrap.demo').controller('ModalDemoCtrl', function ($uibModal, $log, $document) {
+ var $ctrl = this;
+ $ctrl.items = ['item1', 'item2', 'item3'];
- $scope.items = ['item1', 'item2', 'item3'];
+ $ctrl.animationsEnabled = true;
- $scope.animationsEnabled = true;
-
- $scope.open = function (size) {
-
- var modalInstance = $modal.open({
- animation: $scope.animationsEnabled,
+ $ctrl.open = function (size, parentSelector) {
+ var parentElem = parentSelector ?
+ angular.element($document[0].querySelector('.modal-demo ' + parentSelector)) : undefined;
+ var modalInstance = $uibModal.open({
+ animation: $ctrl.animationsEnabled,
+ ariaLabelledBy: 'modal-title',
+ ariaDescribedBy: 'modal-body',
templateUrl: 'myModalContent.html',
controller: 'ModalInstanceCtrl',
+ controllerAs: '$ctrl',
size: size,
+ appendTo: parentElem,
resolve: {
items: function () {
- return $scope.items;
+ return $ctrl.items;
}
}
});
modalInstance.result.then(function (selectedItem) {
- $scope.selected = selectedItem;
+ $ctrl.selected = selectedItem;
}, function () {
$log.info('Modal dismissed at: ' + new Date());
});
};
- $scope.toggleAnimation = function () {
- $scope.animationsEnabled = !$scope.animationsEnabled;
+ $ctrl.openComponentModal = function () {
+ var modalInstance = $uibModal.open({
+ animation: $ctrl.animationsEnabled,
+ component: 'modalComponent',
+ resolve: {
+ items: function () {
+ return $ctrl.items;
+ }
+ }
+ });
+
+ modalInstance.result.then(function (selectedItem) {
+ $ctrl.selected = selectedItem;
+ }, function () {
+ $log.info('modal-component dismissed at: ' + new Date());
+ });
};
-});
+ $ctrl.openMultipleModals = function () {
+ $uibModal.open({
+ animation: $ctrl.animationsEnabled,
+ ariaLabelledBy: 'modal-title-bottom',
+ ariaDescribedBy: 'modal-body-bottom',
+ templateUrl: 'stackedModal.html',
+ size: 'sm',
+ controller: function($scope) {
+ $scope.name = 'bottom';
+ }
+ });
-// Please note that $modalInstance represents a modal window (instance) dependency.
-// It is not the same as the $modal service used above.
+ $uibModal.open({
+ animation: $ctrl.animationsEnabled,
+ ariaLabelledBy: 'modal-title-top',
+ ariaDescribedBy: 'modal-body-top',
+ templateUrl: 'stackedModal.html',
+ size: 'sm',
+ controller: function($scope) {
+ $scope.name = 'top';
+ }
+ });
+ };
+
+ $ctrl.toggleAnimation = function () {
+ $ctrl.animationsEnabled = !$ctrl.animationsEnabled;
+ };
+});
-angular.module('ui.bootstrap.demo').controller('ModalInstanceCtrl', function ($scope, $modalInstance, items) {
+// Please note that $uibModalInstance represents a modal window (instance) dependency.
+// It is not the same as the $uibModal service used above.
- $scope.items = items;
- $scope.selected = {
- item: $scope.items[0]
+angular.module('ui.bootstrap.demo').controller('ModalInstanceCtrl', function ($uibModalInstance, items) {
+ var $ctrl = this;
+ $ctrl.items = items;
+ $ctrl.selected = {
+ item: $ctrl.items[0]
};
- $scope.ok = function () {
- $modalInstance.close($scope.selected.item);
+ $ctrl.ok = function () {
+ $uibModalInstance.close($ctrl.selected.item);
};
- $scope.cancel = function () {
- $modalInstance.dismiss('cancel');
+ $ctrl.cancel = function () {
+ $uibModalInstance.dismiss('cancel');
};
});
+
+// Please note that the close and dismiss bindings are from $uibModalInstance.
+
+angular.module('ui.bootstrap.demo').component('modalComponent', {
+ templateUrl: 'myModalContent.html',
+ bindings: {
+ resolve: '<',
+ close: '&',
+ dismiss: '&'
+ },
+ controller: function () {
+ var $ctrl = this;
+
+ $ctrl.$onInit = function () {
+ $ctrl.items = $ctrl.resolve.items;
+ $ctrl.selected = {
+ item: $ctrl.items[0]
+ };
+ };
+
+ $ctrl.ok = function () {
+ $ctrl.close({$value: $ctrl.selected.item});
+ };
+
+ $ctrl.cancel = function () {
+ $ctrl.dismiss({$value: 'cancel'});
+ };
+ }
+});
diff --git a/src/modal/docs/readme.md b/src/modal/docs/readme.md
index 573b2510f6..66b6f36803 100644
--- a/src/modal/docs/readme.md
+++ b/src/modal/docs/readme.md
@@ -1,38 +1,161 @@
-`$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
-* `animation` - set to false to disable animations on new modal/backdrop. Does not toggle animations for modals/backdrops that are already displayed.
-* `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 suffix of modal window class. The value used is appended to the `modal-` class, i.e. a value of `sm` gives `modal-sm`
+`$uibModal` is a service to create modal windows.
+Creating modals is straightforward: create a template and controller, and reference them when using `$uibModal`.
+
+The `$uibModal` service has only one method: `open(options)`.
+
+### $uibModal's open function
+
+#### options parameter
+
+* `animation`
+ _(Type: `boolean`, Default: `true`)_ -
+ Set to false to disable animations on new modal/backdrop. Does not toggle animations for modals/backdrops that are already displayed.
+
+* `appendTo`
+ _(Type: `angular.element`, Default: `body`: Example: `$document.find('aside').eq(0)`)_ -
+ Appends the modal to a specific element.
+
+* `ariaDescribedBy`
+ _(Type: `string`, `my-modal-description`)_ -
+ Sets the [`aria-describedby`](https://www.w3.org/TR/wai-aria/states_and_properties#aria-describedby) property on the modal. The value should be an id (without the leading `#`) pointing to the element that describes your modal. Typically, this will be the text on your modal, but does not include something the user would interact with, like buttons or a form. Omitting this option will not impact sighted users but will weaken your accessibility support.
+
+* `ariaLabelledBy`
+ _(Type: `string`, `my-modal-title`)_ -
+ Sets the [`aria-labelledby`](https://www.w3.org/TR/wai-aria/states_and_properties#aria-labelledby) property on the modal. The value should be an id (without the leading `#`) pointing to the element that labels your modal. Typically, this will be a header element. Omitting this option will not impact sighted users but will weaken your accessibility support.
+
+* `backdrop`
+ _(Type: `boolean|string`, Default: `true`)_ -
+ Controls presence of a backdrop. Allowed values: `true` (default), `false` (no backdrop), `'static'` (disables modal closing by click on the backdrop).
+
+* `backdropClass`
+ _(Type: `string`)_ -
+ Additional CSS class(es) to be added to a modal backdrop template.
+
+* `bindToController`
+ _(Type: `boolean`, Default: `false`)_ -
+ When used with `controllerAs` & set to `true`, it will bind the $scope properties onto the controller.
+
+* `component`
+ _(Type: `string`, Example: `myComponent`)_ -
+ A string reference to the component to be rendered that is registered with Angular's compiler. If using a directive, the directive must have `restrict: 'E'` and a template or templateUrl set.
+
+ It supports these bindings:
+
+ * `close` - A method that can be used to close a modal, passing a result. The result must be passed in this format: `{$value: myResult}`
+
+ * `dismiss` - A method that can be used to dismiss a modal, passing a result. The result must be passed in this format: `{$value: myRejectedResult}`
+
+ * `modalInstance` - The modal instance. This is the same `$uibModalInstance` injectable found when using `controller`.
+
+ * `resolve` - An object of the modal resolve values. See [UI Router resolves](#ui-router-resolves) for details.
+
+* `controller`
+ _(Type: `function|string|array`, Example: `MyModalController`)_ -
+ A controller for the modal instance, either a controller name as a string, or an inline controller function, optionally wrapped in array notation for dependency injection. Allows the controller-as syntax. Has a special `$uibModalInstance` injectable to access the modal instance.
+
+* `controllerAs`
+ _(Type: `string`, Example: `ctrl`)_ -
+ An alternative to the controller-as syntax. Requires the `controller` option to be provided as well.
+
+* `keyboard` -
+ _(Type: `boolean`, Default: `true`)_ -
+ Indicates whether the dialog should be closable by hitting the ESC key.
+
+* `openedClass`
+ _(Type: `string`, Default: `modal-open`)_ -
+ Class added to the `body` element when the modal is opened.
+
+* `resolve`
+ _(Type: `Object`)_ -
+ Members that will be resolved and passed to the controller as locals; it is equivalent of the `resolve` property in the router.
+
+* `scope`
+ _(Type: `$scope`)_ -
+ The parent scope instance to be used for the modal's content. Defaults to `$rootScope`.
+
+* `size`
+ _(Type: `string`, Example: `lg`)_ -
+ Optional suffix of modal window class. The value used is appended to the `modal-` class, i.e. a value of `sm` gives `modal-sm`.
+
+* `template`
+ _(Type: `string`)_ -
+ Inline template representing the modal's content.
+
+* `templateUrl`
+ _(Type: `string`)_ -
+ A path to a template representing modal's content. You need either a `template` or `templateUrl`.
+
+* `windowClass`
+ _(Type: `string`)_ -
+ Additional CSS class(es) to be added to a modal window template.
+
+* `windowTemplateUrl`
+ _(Type: `string`, Default: `uib/template/modal/window.html`)_ -
+ A path to a template overriding modal's window template.
+
+* `windowTopClass`
+ _(Type: `string`)_ -
+ CSS class(es) to be added to the top modal window.
+
+Global defaults may be set for `$uibModal` via `$uibModalProvider.options`.
+
+#### return
The `open` method returns a modal instance, an object with the following properties:
-* `close(result)` - 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
-* `rendered` - a promise that is resolved when a modal is rendered.
+* `close(result)`
+ _(Type: `function`)_ -
+ Can be used to close a modal, passing a result.
+
+* `dismiss(reason)`
+ _(Type: `function`)_ -
+ Can be used to dismiss a modal, passing a reason.
+
+* `result`
+ _(Type: `promise`)_ -
+ Is resolved when a modal is closed and rejected when a modal is dismissed.
+
+* `opened`
+ _(Type: `promise`)_ -
+ Is resolved when a modal gets opened after downloading content's template and resolving all variables.
-In addition the scope associated with modal's content is augmented with 2 methods:
+* `closed`
+ _(Type: `promise`)_ -
+ Is resolved when a modal is closed and the animation completes.
+
+* `rendered`
+ _(Type: `promise`)_ -
+ Is resolved when a modal is rendered.
+
+---
+
+The scope associated with modal's content is augmented with:
* `$close(result)`
+ _(Type: `function`)_ -
+ A method that can be used to close a modal, passing a result.
+
* `$dismiss(reason)`
+ _(Type: `function`)_ -
+ A method that can be used to dismiss a modal, passing a reason.
Those methods make it easy to close a modal window without a need to create a dedicated controller.
-Finally, a `modal.closing` event is broadcast to the modal scope before the modal closes. If the listener calls
-preventDefault on the event, then the modal will remain open. The $close and $dismiss methods return true if the
-event was allowed. The event itself includes a parameter for the result/reason and a boolean parameter that indicates
-whether the modal is being closed (true) or dismissed.
+Also, when using `bindToController`, you can define an `$onInit` method in the controller that will fire upon initialization.
+
+---
+
+Events fired:
+
+* `$uibUnscheduledDestruction` -
+ This event is fired if the $scope is destroyed via unexpected mechanism, such as it being passed in the modal options and a $route/$state transition occurs. The modal will also be dismissed.
+
+* `modal.closing` -
+ This event is broadcast to the modal scope before the modal closes. If the listener calls preventDefault() on the event, then the modal will remain open.
+ Also, the `$close` and `$dismiss` methods returns true if the event was executed. This event also includes a parameter for the result/reason and a boolean that indicates whether the modal is being closed (true) or dismissed.
+
+##### UI Router resolves
+
+If one wants to have the modal resolve using [UI Router's](https://github.com/angular-ui/ui-router) pre-1.0 resolve mechanism, one can call `$uibResolve.setResolver('$resolve')` in the configuration phase of the application. One can also provide a custom resolver as well, as long as the signature conforms to UI Router's [$resolve](http://angular-ui.github.io/ui-router/site/#/api/ui.router.util.$resolve).
+
+When the modal is opened with a controller, a `$resolve` object is exposed on the template with the resolved values from the resolve object. If using the component option, see details on how to access this object in component section of the modal documentation.
diff --git a/src/modal/index-nocss.js b/src/modal/index-nocss.js
new file mode 100644
index 0000000000..d3f8a94b7d
--- /dev/null
+++ b/src/modal/index-nocss.js
@@ -0,0 +1,11 @@
+require('../multiMap');
+require('../position/index-nocss.js');
+require('../stackedMap');
+require('../../template/modal/window.html.js');
+require('./modal');
+
+var MODULE_NAME = 'ui.bootstrap.module.modal';
+
+angular.module(MODULE_NAME, ['ui.bootstrap.modal', 'uib/template/modal/window.html']);
+
+module.exports = MODULE_NAME;
diff --git a/src/modal/index.js b/src/modal/index.js
new file mode 100644
index 0000000000..93dbeed820
--- /dev/null
+++ b/src/modal/index.js
@@ -0,0 +1,2 @@
+require('../position/position.css');
+module.exports = require('./index-nocss.js');
diff --git a/src/modal/modal.js b/src/modal/modal.js
index 9470f62d5c..679ff188e8 100644
--- a/src/modal/modal.js
+++ b/src/modal/modal.js
@@ -1,182 +1,226 @@
-angular.module('ui.bootstrap.modal', [])
-
+angular.module('ui.bootstrap.modal', ['ui.bootstrap.multiMap', 'ui.bootstrap.stackedMap', 'ui.bootstrap.position'])
/**
- * A helper, internal data structure that acts as a map but also allows getting / removing
- * elements in the LIFO order
+ * Pluggable resolve mechanism for the modal resolve resolution
+ * Supports UI Router's $resolve service
*/
- .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;
- }
- };
- }
+ .provider('$uibResolve', function() {
+ var resolve = this;
+ this.resolver = null;
+
+ this.setResolver = function(resolver) {
+ this.resolver = resolver;
};
+
+ this.$get = ['$injector', '$q', function($injector, $q) {
+ var resolver = resolve.resolver ? $injector.get(resolve.resolver) : null;
+ return {
+ resolve: function(invocables, locals, parent, self) {
+ if (resolver) {
+ return resolver.resolve(invocables, locals, parent, self);
+ }
+
+ var promises = [];
+
+ angular.forEach(invocables, function(value) {
+ if (angular.isFunction(value) || angular.isArray(value)) {
+ promises.push($q.resolve($injector.invoke(value)));
+ } else if (angular.isString(value)) {
+ promises.push($q.resolve($injector.get(value)));
+ } else {
+ promises.push($q.resolve(value));
+ }
+ });
+
+ return $q.all(promises).then(function(resolves) {
+ var resolveObj = {};
+ var resolveIter = 0;
+ angular.forEach(invocables, function(value, key) {
+ resolveObj[key] = resolves[resolveIter++];
+ });
+
+ return resolveObj;
+ });
+ }
+ };
+ }];
})
/**
* A helper directive for the $modal service. It creates a backdrop element.
*/
- .directive('modalBackdrop', ['$timeout', function ($timeout) {
+ .directive('uibModalBackdrop', ['$animate', '$injector', '$uibModalStack',
+ function($animate, $injector, $modalStack) {
return {
- restrict: 'EA',
- replace: true,
- templateUrl: 'template/modal/backdrop.html',
- compile: function (tElement, tAttrs) {
+ restrict: 'A',
+ compile: function(tElement, tAttrs) {
tElement.addClass(tAttrs.backdropClass);
return linkFn;
}
};
function linkFn(scope, element, attrs) {
- scope.animate = false;
+ if (attrs.modalInClass) {
+ $animate.addClass(element, attrs.modalInClass);
- //trigger CSS transitions
- $timeout(function () {
- scope.animate = true;
- });
+ scope.$on($modalStack.NOW_CLOSING_EVENT, function(e, setIsAsync) {
+ var done = setIsAsync();
+ if (scope.modalOptions.animation) {
+ $animate.removeClass(element, attrs.modalInClass).then(done);
+ } else {
+ done();
+ }
+ });
+ }
}
}])
- .directive('modalWindow', ['$modalStack', '$q', function ($modalStack, $q) {
+ .directive('uibModalWindow', ['$uibModalStack', '$q', '$animateCss', '$document',
+ function($modalStack, $q, $animateCss, $document) {
return {
- restrict: 'EA',
scope: {
- index: '@',
- animate: '='
+ index: '@'
},
- replace: true,
+ restrict: 'A',
transclude: true,
templateUrl: function(tElement, tAttrs) {
- return tAttrs.templateUrl || 'template/modal/window.html';
+ return tAttrs.templateUrl || 'uib/template/modal/window.html';
},
- link: function (scope, element, attrs) {
- element.addClass(attrs.windowClass || '');
+ link: function(scope, element, attrs) {
+ element.addClass(attrs.windowTopClass || '');
scope.size = attrs.size;
- scope.close = function (evt) {
+ scope.close = function(evt) {
var modal = $modalStack.getTop();
- if (modal && modal.value.backdrop && modal.value.backdrop != 'static' && (evt.target === evt.currentTarget)) {
+ if (modal && modal.value.backdrop &&
+ modal.value.backdrop !== 'static' &&
+ evt.target === evt.currentTarget) {
evt.preventDefault();
evt.stopPropagation();
$modalStack.dismiss(modal.key, 'backdrop click');
}
};
+ // moved from template to fix issue #2280
+ element.on('click', scope.close);
+
// This property is only added to the scope for the purpose of detecting when this directive is rendered.
// We can detect that by using this property in the template associated with this directive and then use
// {@link Attribute#$observe} on it. For more details please see {@link TableColumnResize}.
scope.$isRendered = true;
- // Deferred object that will be resolved when this modal is render.
+ // Deferred object that will be resolved when this modal is rendered.
var modalRenderDeferObj = $q.defer();
- // Observe function will be called on next digest cycle after compilation, ensuring that the DOM is ready.
- // In order to use this way of finding whether DOM is ready, we need to observe a scope property used in modal's template.
- attrs.$observe('modalRender', function (value) {
- if (value == 'true') {
- modalRenderDeferObj.resolve();
- }
+ // Resolve render promise post-digest
+ scope.$$postDigest(function() {
+ modalRenderDeferObj.resolve();
});
- modalRenderDeferObj.promise.then(function () {
- // trigger CSS transitions
- scope.animate = true;
-
- var inputsWithAutofocus = element[0].querySelectorAll('[autofocus]');
- /**
- * Auto-focusing of a freshly-opened modal element causes any child elements
- * with the autofocus attribute to lose focus. This is an issue on touch
- * based devices which will show and then hide the onscreen keyboard.
- * Attempts to refocus the autofocus element via JavaScript will not reopen
- * the onscreen keyboard. Fixed by updated the focusing logic to only autofocus
- * the modal element if the modal does not contain an autofocus element.
- */
- if (inputsWithAutofocus.length) {
- inputsWithAutofocus[0].focus();
- } else {
- element[0].focus();
- }
+ modalRenderDeferObj.promise.then(function() {
+ var animationPromise = null;
- // Notify {@link $modalStack} that modal is rendered.
- var modal = $modalStack.getTop();
- if (modal) {
- $modalStack.modalRendered(modal.key);
+ if (attrs.modalInClass) {
+ animationPromise = $animateCss(element, {
+ addClass: attrs.modalInClass
+ }).start();
+
+ scope.$on($modalStack.NOW_CLOSING_EVENT, function(e, setIsAsync) {
+ var done = setIsAsync();
+ $animateCss(element, {
+ removeClass: attrs.modalInClass
+ }).start().then(done);
+ });
}
+
+
+ $q.when(animationPromise).then(function() {
+ // Notify {@link $modalStack} that modal is rendered.
+ var modal = $modalStack.getTop();
+ if (modal) {
+ $modalStack.modalRendered(modal.key);
+ }
+
+ /**
+ * If something within the freshly-opened modal already has focus (perhaps via a
+ * directive that causes focus) then there's no need to try to focus anything.
+ */
+ if (!($document[0].activeElement && element[0].contains($document[0].activeElement))) {
+ var inputWithAutofocus = element[0].querySelector('[autofocus]');
+ /**
+ * Auto-focusing of a freshly-opened modal element causes any child elements
+ * with the autofocus attribute to lose focus. This is an issue on touch
+ * based devices which will show and then hide the onscreen keyboard.
+ * Attempts to refocus the autofocus element via JavaScript will not reopen
+ * the onscreen keyboard. Fixed by updated the focusing logic to only autofocus
+ * the modal element if the modal does not contain an autofocus element.
+ */
+ if (inputWithAutofocus) {
+ inputWithAutofocus.focus();
+ } else {
+ element[0].focus();
+ }
+ }
+ });
});
}
};
}])
- .directive('modalAnimationClass', [
- function () {
- return {
- compile: function (tElement, tAttrs) {
- if (tAttrs.modalAnimation) {
- tElement.addClass(tAttrs.modalAnimationClass);
- }
+ .directive('uibModalAnimationClass', function() {
+ return {
+ compile: function(tElement, tAttrs) {
+ if (tAttrs.modalAnimation) {
+ tElement.addClass(tAttrs.uibModalAnimationClass);
}
- };
- }])
+ }
+ };
+ })
- .directive('modalTransclude', function () {
+ .directive('uibModalTransclude', ['$animate', function($animate) {
return {
- link: function($scope, $element, $attrs, controller, $transclude) {
- $transclude($scope.$parent, function(clone) {
- $element.empty();
- $element.append(clone);
+ link: function(scope, element, attrs, controller, transclude) {
+ transclude(scope.$parent, function(clone) {
+ element.empty();
+ $animate.enter(clone, element);
});
}
};
- })
-
- .factory('$modalStack', ['$animate', '$timeout', '$document', '$compile', '$rootScope', '$$stackedMap',
- function ($animate, $timeout, $document, $compile, $rootScope, $$stackedMap) {
+ }])
+ .factory('$uibModalStack', ['$animate', '$animateCss', '$document',
+ '$compile', '$rootScope', '$q', '$$multiMap', '$$stackedMap', '$uibPosition',
+ function($animate, $animateCss, $document, $compile, $rootScope, $q, $$multiMap, $$stackedMap, $uibPosition) {
var OPENED_MODAL_CLASS = 'modal-open';
var backdropDomEl, backdropScope;
var openedWindows = $$stackedMap.createNew();
- var $modalStack = {};
+ var openedClasses = $$multiMap.createNew();
+ var $modalStack = {
+ NOW_CLOSING_EVENT: 'modal.stack.now-closing'
+ };
+ var topModalIndex = 0;
+ var previousTopOpenedModal = null;
+ var ARIA_HIDDEN_ATTRIBUTE_NAME = 'data-bootstrap-modal-aria-hidden-count';
+
+ //Modal focus behavior
+ var tabbableSelector = 'a[href], area[href], input:not([disabled]):not([tabindex=\'-1\']), ' +
+ 'button:not([disabled]):not([tabindex=\'-1\']),select:not([disabled]):not([tabindex=\'-1\']), textarea:not([disabled]):not([tabindex=\'-1\']), ' +
+ 'iframe, object, embed, *[tabindex]:not([tabindex=\'-1\']), *[contenteditable=true]';
+ var scrollbarPadding;
+ var SNAKE_CASE_REGEXP = /[A-Z]/g;
+
+ // TODO: extract into common dependency with tooltip
+ function snake_case(name) {
+ var separator = '-';
+ return name.replace(SNAKE_CASE_REGEXP, function(letter, pos) {
+ return (pos ? separator : '') + letter.toLowerCase();
+ });
+ }
+
+ function isVisible(element) {
+ return !!(element.offsetWidth ||
+ element.offsetHeight ||
+ element.getClientRects().length);
+ }
function backdropIndex() {
var topBackdropIndex = -1;
@@ -186,55 +230,98 @@ angular.module('ui.bootstrap.modal', [])
topBackdropIndex = i;
}
}
+
+ // If any backdrop exist, ensure that it's index is always
+ // right below the top modal
+ if (topBackdropIndex > -1 && topBackdropIndex < topModalIndex) {
+ topBackdropIndex = topModalIndex;
+ }
return topBackdropIndex;
}
- $rootScope.$watch(backdropIndex, function(newBackdropIndex){
+ $rootScope.$watch(backdropIndex, function(newBackdropIndex) {
if (backdropScope) {
backdropScope.index = newBackdropIndex;
}
});
- function removeModalWindow(modalInstance) {
-
- var body = $document.find('body').eq(0);
+ function removeModalWindow(modalInstance, elementToReceiveFocus) {
var modalWindow = openedWindows.get(modalInstance).value;
+ var appendToElement = modalWindow.appendTo;
//clean up the stack
openedWindows.remove(modalInstance);
+ previousTopOpenedModal = openedWindows.top();
+ if (previousTopOpenedModal) {
+ topModalIndex = parseInt(previousTopOpenedModal.value.modalDomEl.attr('index'), 10);
+ }
- //remove window DOM element
removeAfterAnimate(modalWindow.modalDomEl, modalWindow.modalScope, function() {
- 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, function () {
- backdropScopeRef = null;
- });
- backdropDomEl = undefined;
- backdropScope = undefined;
+ var modalBodyClass = modalWindow.openedClass || OPENED_MODAL_CLASS;
+ openedClasses.remove(modalBodyClass, modalInstance);
+ var areAnyOpen = openedClasses.hasKey(modalBodyClass);
+ appendToElement.toggleClass(modalBodyClass, areAnyOpen);
+ if (!areAnyOpen && scrollbarPadding && scrollbarPadding.heightOverflow && scrollbarPadding.scrollbarWidth) {
+ if (scrollbarPadding.originalRight) {
+ appendToElement.css({paddingRight: scrollbarPadding.originalRight + 'px'});
+ } else {
+ appendToElement.css({paddingRight: ''});
+ }
+ scrollbarPadding = null;
}
+ toggleTopWindowClass(true);
+ }, modalWindow.closedDeferred);
+ checkRemoveBackdrop();
+
+ //move focus to specified element if available, or else to body
+ if (elementToReceiveFocus && elementToReceiveFocus.focus) {
+ elementToReceiveFocus.focus();
+ } else if (appendToElement.focus) {
+ appendToElement.focus();
+ }
}
- function removeAfterAnimate(domEl, scope, done) {
- // Closing animation
- scope.animate = false;
+ // Add or remove "windowTopClass" from the top window in the stack
+ function toggleTopWindowClass(toggleSwitch) {
+ var modalWindow;
- if (domEl.attr('modal-animation') && $animate.enabled()) {
- // transition out
- domEl.one('$animate:close', function closeFn() {
- $rootScope.$evalAsync(afterAnimating);
+ if (openedWindows.length() > 0) {
+ modalWindow = openedWindows.top().value;
+ modalWindow.modalDomEl.toggleClass(modalWindow.windowTopClass || '', toggleSwitch);
+ }
+ }
+
+ function checkRemoveBackdrop() {
+ //remove backdrop if no longer needed
+ if (backdropDomEl && backdropIndex() === -1) {
+ var backdropScopeRef = backdropScope;
+ removeAfterAnimate(backdropDomEl, backdropScope, function() {
+ backdropScopeRef = null;
});
- } else {
- // Ensure this call is async
- $timeout(afterAnimating);
+ backdropDomEl = undefined;
+ backdropScope = undefined;
}
+ }
+
+ function removeAfterAnimate(domEl, scope, done, closedDeferred) {
+ var asyncDeferred;
+ var asyncPromise = null;
+ var setIsAsync = function() {
+ if (!asyncDeferred) {
+ asyncDeferred = $q.defer();
+ asyncPromise = asyncDeferred.promise;
+ }
+
+ return function asyncDone() {
+ asyncDeferred.resolve();
+ };
+ };
+ scope.$broadcast($modalStack.NOW_CLOSING_EVENT, setIsAsync);
+
+ // Note that it's intentional that asyncPromise might be null.
+ // That's when setIsAsync has not been called during the
+ // NOW_CLOSING_EVENT broadcast.
+ return $q.when(asyncPromise).then(afterAnimating);
function afterAnimating() {
if (afterAnimating.done) {
@@ -242,159 +329,358 @@ angular.module('ui.bootstrap.modal', [])
}
afterAnimating.done = true;
- domEl.remove();
+ $animate.leave(domEl).then(function() {
+ if (done) {
+ done();
+ }
+
+ domEl.remove();
+ if (closedDeferred) {
+ closedDeferred.resolve();
+ }
+ });
+
scope.$destroy();
- if (done) {
- done();
- }
}
}
- $document.bind('keydown', function (evt) {
- var modal;
+ $document.on('keydown', keydownListener);
- if (evt.which === 27) {
- modal = openedWindows.top();
- if (modal && modal.value.keyboard) {
- evt.preventDefault();
- $rootScope.$apply(function () {
- $modalStack.dismiss(modal.key, 'escape key press');
- });
+ $rootScope.$on('$destroy', function() {
+ $document.off('keydown', keydownListener);
+ });
+
+ function keydownListener(evt) {
+ if (evt.isDefaultPrevented()) {
+ return evt;
+ }
+
+ var modal = openedWindows.top();
+ if (modal) {
+ switch (evt.which) {
+ case 27: {
+ if (modal.value.keyboard) {
+ evt.preventDefault();
+ $rootScope.$apply(function() {
+ $modalStack.dismiss(modal.key, 'escape key press');
+ });
+ }
+ break;
+ }
+ case 9: {
+ var list = $modalStack.loadFocusElementList(modal);
+ var focusChanged = false;
+ if (evt.shiftKey) {
+ if ($modalStack.isFocusInFirstItem(evt, list) || $modalStack.isModalFocused(evt, modal)) {
+ focusChanged = $modalStack.focusLastFocusableElement(list);
+ }
+ } else {
+ if ($modalStack.isFocusInLastItem(evt, list)) {
+ focusChanged = $modalStack.focusFirstFocusableElement(list);
+ }
+ }
+
+ if (focusChanged) {
+ evt.preventDefault();
+ evt.stopPropagation();
+ }
+
+ break;
+ }
}
}
- });
+ }
+
+ $modalStack.open = function(modalInstance, modal) {
+ var modalOpener = $document[0].activeElement,
+ modalBodyClass = modal.openedClass || OPENED_MODAL_CLASS;
- $modalStack.open = function (modalInstance, modal) {
+ toggleTopWindowClass(false);
- var modalOpener = $document[0].activeElement;
+ // Store the current top first, to determine what index we ought to use
+ // for the current top modal
+ previousTopOpenedModal = openedWindows.top();
openedWindows.add(modalInstance, {
deferred: modal.deferred,
renderDeferred: modal.renderDeferred,
+ closedDeferred: modal.closedDeferred,
modalScope: modal.scope,
backdrop: modal.backdrop,
- keyboard: modal.keyboard
+ keyboard: modal.keyboard,
+ openedClass: modal.openedClass,
+ windowTopClass: modal.windowTopClass,
+ animation: modal.animation,
+ appendTo: modal.appendTo
});
- var body = $document.find('body').eq(0),
+ openedClasses.put(modalBodyClass, modalInstance);
+
+ var appendToElement = modal.appendTo,
currBackdropIndex = backdropIndex();
if (currBackdropIndex >= 0 && !backdropDomEl) {
backdropScope = $rootScope.$new(true);
+ backdropScope.modalOptions = modal;
backdropScope.index = currBackdropIndex;
- var angularBackgroundDomEl = angular.element('');
- angularBackgroundDomEl.attr('backdrop-class', modal.backdropClass);
+ backdropDomEl = angular.element('');
+ backdropDomEl.attr({
+ 'class': 'modal-backdrop',
+ 'ng-style': '{\'z-index\': 1040 + (index && 1 || 0) + index*10}',
+ 'uib-modal-animation-class': 'fade',
+ 'modal-in-class': 'in'
+ });
+ if (modal.backdropClass) {
+ backdropDomEl.addClass(modal.backdropClass);
+ }
+
if (modal.animation) {
- angularBackgroundDomEl.attr('modal-animation', 'true');
+ backdropDomEl.attr('modal-animation', 'true');
+ }
+ $compile(backdropDomEl)(backdropScope);
+ $animate.enter(backdropDomEl, appendToElement);
+ if ($uibPosition.isScrollable(appendToElement)) {
+ scrollbarPadding = $uibPosition.scrollbarPadding(appendToElement);
+ if (scrollbarPadding.heightOverflow && scrollbarPadding.scrollbarWidth) {
+ appendToElement.css({paddingRight: scrollbarPadding.right + 'px'});
+ }
}
- backdropDomEl = $compile(angularBackgroundDomEl)(backdropScope);
- body.append(backdropDomEl);
}
- var angularDomEl = angular.element('');
+ var content;
+ if (modal.component) {
+ content = document.createElement(snake_case(modal.component.name));
+ content = angular.element(content);
+ content.attr({
+ resolve: '$resolve',
+ 'modal-instance': '$uibModalInstance',
+ close: '$close($value)',
+ dismiss: '$dismiss($value)'
+ });
+ } else {
+ content = modal.content;
+ }
+
+ // Set the top modal index based on the index of the previous top modal
+ topModalIndex = previousTopOpenedModal ? parseInt(previousTopOpenedModal.value.modalDomEl.attr('index'), 10) + 1 : 0;
+ var angularDomEl = angular.element('');
angularDomEl.attr({
+ 'class': 'modal',
'template-url': modal.windowTemplateUrl,
- 'window-class': modal.windowClass,
+ 'window-top-class': modal.windowTopClass,
+ 'role': 'dialog',
+ 'aria-labelledby': modal.ariaLabelledBy,
+ 'aria-describedby': modal.ariaDescribedBy,
'size': modal.size,
- 'index': openedWindows.length() - 1,
- 'animate': 'animate'
- }).html(modal.content);
+ 'index': topModalIndex,
+ 'animate': 'animate',
+ 'ng-style': '{\'z-index\': 1050 + $$topModalIndex*10, display: \'block\'}',
+ 'tabindex': -1,
+ 'uib-modal-animation-class': 'fade',
+ 'modal-in-class': 'in'
+ }).append(content);
+ if (modal.windowClass) {
+ angularDomEl.addClass(modal.windowClass);
+ }
+
if (modal.animation) {
angularDomEl.attr('modal-animation', 'true');
}
- var modalDomEl = $compile(angularDomEl)(modal.scope);
- openedWindows.top().value.modalDomEl = modalDomEl;
+ appendToElement.addClass(modalBodyClass);
+ if (modal.scope) {
+ // we need to explicitly add the modal index to the modal scope
+ // because it is needed by ngStyle to compute the zIndex property.
+ modal.scope.$$topModalIndex = topModalIndex;
+ }
+ $animate.enter($compile(angularDomEl)(modal.scope), appendToElement);
+
+ openedWindows.top().value.modalDomEl = angularDomEl;
openedWindows.top().value.modalOpener = modalOpener;
- body.append(modalDomEl);
- body.addClass(OPENED_MODAL_CLASS);
+
+ applyAriaHidden(angularDomEl);
+
+ function applyAriaHidden(el) {
+ if (!el || el[0].tagName === 'BODY') {
+ return;
+ }
+
+ getSiblings(el).forEach(function(sibling) {
+ var elemIsAlreadyHidden = sibling.getAttribute('aria-hidden') === 'true',
+ ariaHiddenCount = parseInt(sibling.getAttribute(ARIA_HIDDEN_ATTRIBUTE_NAME), 10);
+
+ if (!ariaHiddenCount) {
+ ariaHiddenCount = elemIsAlreadyHidden ? 1 : 0;
+ }
+
+ sibling.setAttribute(ARIA_HIDDEN_ATTRIBUTE_NAME, ariaHiddenCount + 1);
+ sibling.setAttribute('aria-hidden', 'true');
+ });
+
+ return applyAriaHidden(el.parent());
+
+ function getSiblings(el) {
+ var children = el.parent() ? el.parent().children() : [];
+
+ return Array.prototype.filter.call(children, function(child) {
+ return child !== el[0];
+ });
+ }
+ }
};
function broadcastClosing(modalWindow, resultOrReason, closing) {
- return !modalWindow.value.modalScope.$broadcast('modal.closing', resultOrReason, closing).defaultPrevented;
+ return !modalWindow.value.modalScope.$broadcast('modal.closing', resultOrReason, closing).defaultPrevented;
+ }
+
+ function unhideBackgroundElements() {
+ Array.prototype.forEach.call(
+ document.querySelectorAll('[' + ARIA_HIDDEN_ATTRIBUTE_NAME + ']'),
+ function(hiddenEl) {
+ var ariaHiddenCount = parseInt(hiddenEl.getAttribute(ARIA_HIDDEN_ATTRIBUTE_NAME), 10),
+ newHiddenCount = ariaHiddenCount - 1;
+ hiddenEl.setAttribute(ARIA_HIDDEN_ATTRIBUTE_NAME, newHiddenCount);
+
+ if (!newHiddenCount) {
+ hiddenEl.removeAttribute(ARIA_HIDDEN_ATTRIBUTE_NAME);
+ hiddenEl.removeAttribute('aria-hidden');
+ }
+ }
+ );
}
- $modalStack.close = function (modalInstance, result) {
+ $modalStack.close = function(modalInstance, result) {
var modalWindow = openedWindows.get(modalInstance);
+ unhideBackgroundElements();
if (modalWindow && broadcastClosing(modalWindow, result, true)) {
+ modalWindow.value.modalScope.$$uibDestructionScheduled = true;
modalWindow.value.deferred.resolve(result);
- removeModalWindow(modalInstance);
- modalWindow.value.modalOpener.focus();
+ removeModalWindow(modalInstance, modalWindow.value.modalOpener);
return true;
}
+
return !modalWindow;
};
- $modalStack.dismiss = function (modalInstance, reason) {
+ $modalStack.dismiss = function(modalInstance, reason) {
var modalWindow = openedWindows.get(modalInstance);
+ unhideBackgroundElements();
if (modalWindow && broadcastClosing(modalWindow, reason, false)) {
+ modalWindow.value.modalScope.$$uibDestructionScheduled = true;
modalWindow.value.deferred.reject(reason);
- removeModalWindow(modalInstance);
- modalWindow.value.modalOpener.focus();
+ removeModalWindow(modalInstance, modalWindow.value.modalOpener);
return true;
}
return !modalWindow;
};
- $modalStack.dismissAll = function (reason) {
+ $modalStack.dismissAll = function(reason) {
var topModal = this.getTop();
while (topModal && this.dismiss(topModal.key, reason)) {
topModal = this.getTop();
}
};
- $modalStack.getTop = function () {
+ $modalStack.getTop = function() {
return openedWindows.top();
};
- $modalStack.modalRendered = function (modalInstance) {
+ $modalStack.modalRendered = function(modalInstance) {
var modalWindow = openedWindows.get(modalInstance);
if (modalWindow) {
modalWindow.value.renderDeferred.resolve();
}
};
+ $modalStack.focusFirstFocusableElement = function(list) {
+ if (list.length > 0) {
+ list[0].focus();
+ return true;
+ }
+ return false;
+ };
+
+ $modalStack.focusLastFocusableElement = function(list) {
+ if (list.length > 0) {
+ list[list.length - 1].focus();
+ return true;
+ }
+ return false;
+ };
+
+ $modalStack.isModalFocused = function(evt, modalWindow) {
+ if (evt && modalWindow) {
+ var modalDomEl = modalWindow.value.modalDomEl;
+ if (modalDomEl && modalDomEl.length) {
+ return (evt.target || evt.srcElement) === modalDomEl[0];
+ }
+ }
+ return false;
+ };
+
+ $modalStack.isFocusInFirstItem = function(evt, list) {
+ if (list.length > 0) {
+ return (evt.target || evt.srcElement) === list[0];
+ }
+ return false;
+ };
+
+ $modalStack.isFocusInLastItem = function(evt, list) {
+ if (list.length > 0) {
+ return (evt.target || evt.srcElement) === list[list.length - 1];
+ }
+ return false;
+ };
+
+ $modalStack.loadFocusElementList = function(modalWindow) {
+ if (modalWindow) {
+ var modalDomE1 = modalWindow.value.modalDomEl;
+ if (modalDomE1 && modalDomE1.length) {
+ var elements = modalDomE1[0].querySelectorAll(tabbableSelector);
+ return elements ?
+ Array.prototype.filter.call(elements, function(element) {
+ return isVisible(element);
+ }) : elements;
+ }
+ }
+ };
+
return $modalStack;
}])
- .provider('$modal', function () {
-
+ .provider('$uibModal', function() {
var $modalProvider = {
options: {
animation: true,
backdrop: true, //can also be false or 'static'
keyboard: true
},
- $get: ['$injector', '$rootScope', '$q', '$templateRequest', '$controller', '$modalStack',
- function ($injector, $rootScope, $q, $templateRequest, $controller, $modalStack) {
-
+ $get: ['$rootScope', '$q', '$document', '$templateRequest', '$controller', '$uibResolve', '$uibModalStack',
+ function ($rootScope, $q, $document, $templateRequest, $controller, $uibResolve, $modalStack) {
var $modal = {};
function getTemplatePromise(options) {
return options.template ? $q.when(options.template) :
- $templateRequest(angular.isFunction(options.templateUrl) ? (options.templateUrl)() : options.templateUrl);
- }
-
- 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;
+ $templateRequest(angular.isFunction(options.templateUrl) ?
+ options.templateUrl() : options.templateUrl);
}
- $modal.open = function (modalOptions) {
+ var promiseChain = null;
+ $modal.getPromiseChain = function() {
+ return promiseChain;
+ };
+ $modal.open = function(modalOptions) {
var modalResultDeferred = $q.defer();
var modalOpenedDeferred = $q.defer();
+ var modalClosedDeferred = $q.defer();
var modalRenderDeferred = $q.defer();
//prepare an instance of a modal to be injected into controllers and returned to a caller
var modalInstance = {
result: modalResultDeferred.promise,
opened: modalOpenedDeferred.promise,
+ closed: modalClosedDeferred.promise,
rendered: modalRenderDeferred.promise,
close: function (result) {
return $modalStack.close(modalInstance, result);
@@ -407,68 +693,137 @@ angular.module('ui.bootstrap.modal', [])
//merge and clean up options
modalOptions = angular.extend({}, $modalProvider.options, modalOptions);
modalOptions.resolve = modalOptions.resolve || {};
+ modalOptions.appendTo = modalOptions.appendTo || $document.find('body').eq(0);
- //verify options
- if (!modalOptions.template && !modalOptions.templateUrl) {
- throw new Error('One of template or templateUrl options is required.');
+ if (!modalOptions.appendTo.length) {
+ throw new Error('appendTo element not found. Make sure that the element passed is in DOM.');
}
- var templateAndResolvePromise =
- $q.all([getTemplatePromise(modalOptions)].concat(getResolvePromises(modalOptions.resolve)));
-
-
- templateAndResolvePromise.then(function resolveSuccess(tplAndVars) {
+ //verify options
+ if (!modalOptions.component && !modalOptions.template && !modalOptions.templateUrl) {
+ throw new Error('One of component or template or templateUrl options is required.');
+ }
- var modalScope = (modalOptions.scope || $rootScope).$new();
- modalScope.$close = modalInstance.close;
- modalScope.$dismiss = modalInstance.dismiss;
+ var templateAndResolvePromise;
+ if (modalOptions.component) {
+ templateAndResolvePromise = $q.when($uibResolve.resolve(modalOptions.resolve, {}, null, null));
+ } else {
+ templateAndResolvePromise =
+ $q.all([getTemplatePromise(modalOptions), $uibResolve.resolve(modalOptions.resolve, {}, null, null)]);
+ }
- var ctrlInstance, ctrlLocals = {};
- var resolveIter = 1;
+ function resolveWithTemplate() {
+ return templateAndResolvePromise;
+ }
- //controllers
- if (modalOptions.controller) {
- ctrlLocals.$scope = modalScope;
- ctrlLocals.$modalInstance = modalInstance;
- angular.forEach(modalOptions.resolve, function (value, key) {
- ctrlLocals[key] = tplAndVars[resolveIter++];
+ // Wait for the resolution of the existing promise chain.
+ // Then switch to our own combined promise dependency (regardless of how the previous modal fared).
+ // Then add to $modalStack and resolve opened.
+ // Finally clean up the chain variable if no subsequent modal has overwritten it.
+ var samePromise;
+ samePromise = promiseChain = $q.all([promiseChain])
+ .then(resolveWithTemplate, resolveWithTemplate)
+ .then(function resolveSuccess(tplAndVars) {
+ var providedScope = modalOptions.scope || $rootScope;
+
+ var modalScope = providedScope.$new();
+ modalScope.$close = modalInstance.close;
+ modalScope.$dismiss = modalInstance.dismiss;
+
+ modalScope.$on('$destroy', function() {
+ if (!modalScope.$$uibDestructionScheduled) {
+ modalScope.$dismiss('$uibUnscheduledDestruction');
+ }
});
- ctrlInstance = $controller(modalOptions.controller, ctrlLocals);
- if (modalOptions.controllerAs) {
- modalScope[modalOptions.controllerAs] = ctrlInstance;
+ var modal = {
+ scope: modalScope,
+ deferred: modalResultDeferred,
+ renderDeferred: modalRenderDeferred,
+ closedDeferred: modalClosedDeferred,
+ animation: modalOptions.animation,
+ backdrop: modalOptions.backdrop,
+ keyboard: modalOptions.keyboard,
+ backdropClass: modalOptions.backdropClass,
+ windowTopClass: modalOptions.windowTopClass,
+ windowClass: modalOptions.windowClass,
+ windowTemplateUrl: modalOptions.windowTemplateUrl,
+ ariaLabelledBy: modalOptions.ariaLabelledBy,
+ ariaDescribedBy: modalOptions.ariaDescribedBy,
+ size: modalOptions.size,
+ openedClass: modalOptions.openedClass,
+ appendTo: modalOptions.appendTo
+ };
+
+ var component = {};
+ var ctrlInstance, ctrlInstantiate, ctrlLocals = {};
+
+ if (modalOptions.component) {
+ constructLocals(component, false, true, false);
+ component.name = modalOptions.component;
+ modal.component = component;
+ } else if (modalOptions.controller) {
+ constructLocals(ctrlLocals, true, false, true);
+
+ // the third param will make the controller instantiate later,private api
+ // @see https://github.com/angular/angular.js/blob/master/src/ng/controller.js#L126
+ ctrlInstantiate = $controller(modalOptions.controller, ctrlLocals, true, modalOptions.controllerAs);
+ if (modalOptions.controllerAs && modalOptions.bindToController) {
+ ctrlInstance = ctrlInstantiate.instance;
+ ctrlInstance.$close = modalScope.$close;
+ ctrlInstance.$dismiss = modalScope.$dismiss;
+ angular.extend(ctrlInstance, {
+ $resolve: ctrlLocals.$scope.$resolve
+ }, providedScope);
+ }
+
+ ctrlInstance = ctrlInstantiate();
+
+ if (angular.isFunction(ctrlInstance.$onInit)) {
+ ctrlInstance.$onInit();
+ }
}
- }
- $modalStack.open(modalInstance, {
- scope: modalScope,
- deferred: modalResultDeferred,
- renderDeferred: modalRenderDeferred,
- content: tplAndVars[0],
- animation: modalOptions.animation,
- backdrop: modalOptions.backdrop,
- keyboard: modalOptions.keyboard,
- backdropClass: modalOptions.backdropClass,
- windowClass: modalOptions.windowClass,
- windowTemplateUrl: modalOptions.windowTemplateUrl,
- size: modalOptions.size
- });
+ if (!modalOptions.component) {
+ modal.content = tplAndVars[0];
+ }
+ $modalStack.open(modalInstance, modal);
+ modalOpenedDeferred.resolve(true);
+
+ function constructLocals(obj, template, instanceOnScope, injectable) {
+ obj.$scope = modalScope;
+ obj.$scope.$resolve = {};
+ if (instanceOnScope) {
+ obj.$scope.$uibModalInstance = modalInstance;
+ } else {
+ obj.$uibModalInstance = modalInstance;
+ }
+
+ var resolves = template ? tplAndVars[1] : tplAndVars;
+ angular.forEach(resolves, function(value, key) {
+ if (injectable) {
+ obj[key] = value;
+ }
+
+ obj.$scope.$resolve[key] = value;
+ });
+ }
}, function resolveError(reason) {
- modalResultDeferred.reject(reason);
- });
-
- templateAndResolvePromise.then(function () {
- modalOpenedDeferred.resolve(true);
- }, function (reason) {
modalOpenedDeferred.reject(reason);
+ modalResultDeferred.reject(reason);
+ })['finally'](function() {
+ if (promiseChain === samePromise) {
+ promiseChain = null;
+ }
});
return modalInstance;
};
return $modal;
- }]
+ }
+ ]
};
return $modalProvider;
diff --git a/src/modal/test/modal.spec.js b/src/modal/test/modal.spec.js
index c1d5fe4c29..4bf85243da 100644
--- a/src/modal/test/modal.spec.js
+++ b/src/modal/test/modal.spec.js
@@ -1,36 +1,167 @@
-describe('$modal', function () {
- var $controllerProvider, $rootScope, $document, $compile, $templateCache, $timeout, $q;
- var $modal, $modalProvider;
+describe('$uibResolve', function() {
+ beforeEach(module('ui.bootstrap.modal'));
- var triggerKeyDown = function (element, keyCode) {
- var e = $.Event('keydown');
- e.which = keyCode;
- element.trigger(e);
- };
+ it('should resolve invocables and return promise with object of resolutions', function() {
+ module(function($provide) {
+ $provide.factory('bar', function() {
+ return 'bar';
+ });
+ });
+
+ inject(function($q, $rootScope, $uibResolve) {
+ $uibResolve.resolve({
+ foo: 'bar',
+ bar: $q.resolve('baz'),
+ baz: function() {
+ return 'boo';
+ }
+ }).then(function(resolves) {
+ expect(resolves).toEqual({
+ foo: 'bar',
+ bar: 'baz',
+ baz: 'boo'
+ });
+ });
+
+ $rootScope.$digest();
+ });
+ });
+
+ describe('with custom resolver', function() {
+ beforeEach(module(function($provide, $uibResolveProvider) {
+ $provide.factory('$resolve', function() {
+ return {
+ resolve: jasmine.createSpy()
+ };
+ });
+
+ $uibResolveProvider.setResolver('$resolve');
+ }));
+
+ it('should call $resolve.resolve', inject(function($uibResolve, $resolve) {
+ $uibResolve.resolve({foo: 'bar'}, {}, null, null);
+
+ expect($resolve.resolve).toHaveBeenCalledWith({foo: 'bar'}, {}, null, null);
+ }));
+ });
+});
+
+describe('uibModalTransclude', function() {
+ var uibModalTranscludeDDO,
+ $animate;
beforeEach(module('ui.bootstrap.modal'));
- beforeEach(module('template/modal/backdrop.html'));
- beforeEach(module('template/modal/window.html'));
- beforeEach(module(function(_$controllerProvider_, _$modalProvider_){
+ beforeEach(module(function($provide) {
+ $animate = jasmine.createSpyObj('$animate', ['enter']);
+ $provide.value('$animate', $animate);
+ }));
+
+ beforeEach(inject(function(uibModalTranscludeDirective) {
+ uibModalTranscludeDDO = uibModalTranscludeDirective[0];
+ }));
+
+ describe('when initialised', function() {
+ var scope,
+ element,
+ transcludeSpy,
+ transcludeFn;
+
+ beforeEach(function() {
+ scope = {
+ $parent: 'parentScope'
+ };
+
+ element = jasmine.createSpyObj('containerElement', ['empty']);
+ transcludeSpy = jasmine.createSpy('transcludeSpy').and.callFake(function(scope, fn) {
+ transcludeFn = fn;
+ });
+
+ uibModalTranscludeDDO.link(scope, element, {}, {}, transcludeSpy);
+ });
+
+ it('should call the transclusion function', function() {
+ expect(transcludeSpy).toHaveBeenCalledWith(scope.$parent, jasmine.any(Function));
+ });
+
+ describe('transclusion callback', function() {
+ var transcludedContent;
+
+ beforeEach(function() {
+ transcludedContent = 'my transcluded content';
+ transcludeFn(transcludedContent);
+ });
+
+ it('should empty the element', function() {
+ expect(element.empty).toHaveBeenCalledWith();
+ });
+
+ it('should append the transcluded content', function() {
+ expect($animate.enter).toHaveBeenCalledWith(transcludedContent, element);
+ });
+ });
+ });
+});
+
+describe('$uibModal', function() {
+ var $animate, $controllerProvider, $rootScope, $document, $compile, $templateCache, $timeout, $q;
+ var $uibModal, $uibModalStack, $uibModalProvider;
+
+ beforeEach(module('ngAnimateMock'));
+ beforeEach(module('ui.bootstrap.modal'));
+ beforeEach(module('uib/template/modal/window.html'));
+ beforeEach(module(function(_$controllerProvider_, _$uibModalProvider_, $compileProvider) {
$controllerProvider = _$controllerProvider_;
- $modalProvider = _$modalProvider_;
+ $uibModalProvider = _$uibModalProvider_;
+ $compileProvider.directive('parentDirective', function() {
+ return {
+ controller: function() {
+ this.text = 'foo';
+ }
+ };
+ }).directive('childDirective', function() {
+ return {
+ require: '^parentDirective',
+ link: function(scope, elem, attrs, ctrl) {
+ scope.text = ctrl.text;
+ }
+ };
+ }).directive('focusMe', function() {
+ return {
+ link: function(scope, elem, attrs) {
+ elem.focus();
+ }
+ };
+ }).component('fooBar', {
+ bindings: {
+ resolve: '<',
+ modalInstance: '<',
+ close: '&',
+ dismiss: '&'
+ },
+ controller: angular.noop,
+ controllerAs: 'foobar',
+ template: '
Foo Bar
'
+ });
}));
- beforeEach(inject(function (_$rootScope_, _$document_, _$compile_, _$templateCache_, _$timeout_, _$q_, _$modal_) {
+ beforeEach(inject(function(_$animate_, _$rootScope_, _$document_, _$compile_, _$templateCache_, _$timeout_, _$q_, _$uibModal_, _$uibModalStack_) {
+ $animate = _$animate_;
$rootScope = _$rootScope_;
$document = _$document_;
$compile = _$compile_;
$templateCache = _$templateCache_;
$timeout = _$timeout_;
$q = _$q_;
- $modal = _$modal_;
+ $uibModal = _$uibModal_;
+ $uibModalStack = _$uibModalStack_;
}));
- beforeEach(function () {
+ beforeEach(function() {
jasmine.addMatchers({
toBeResolvedWith: function(util, customEqualityTesters) {
return {
compare: function(promise, expected) {
+ var called = false;
promise.then(function(result) {
expect(result).toEqual(expected);
@@ -39,10 +170,18 @@ describe('$modal', function () {
} else {
result.message = 'Expected "' + angular.mock.dump(result) + '" to be resolved with "' + expected + '".';
}
+ }, function(result) {
+ fail('Expected "' + angular.mock.dump(result) + '" to be resolved with "' + expected + '".');
+ })['finally'](function() {
+ called = true;
});
$rootScope.$digest();
+ if (!called) {
+ fail('Expected "' + angular.mock.dump(result) + '" to be resolved with "' + expected + '".');
+ }
+
return {pass: true};
}
};
@@ -51,9 +190,10 @@ describe('$modal', function () {
return {
compare: function(promise, expected) {
var result = {};
+ var called = false;
- promise.then(function() {
-
+ promise.then(function(result) {
+ fail('Expected "' + angular.mock.dump(result) + '" to be rejected with "' + expected + '".');
}, function(result) {
expect(result).toEqual(expected);
@@ -62,10 +202,16 @@ describe('$modal', function () {
} else {
result.message = 'Expected "' + angular.mock.dump(result) + '" to be rejected with "' + expected + '".';
}
+ })['finally'](function() {
+ called = true;
});
$rootScope.$digest();
+ if (!called) {
+ fail('Expected "' + angular.mock.dump(result) + '" to be rejected with "' + expected + '".');
+ }
+
return {pass: true};
}
};
@@ -137,36 +283,58 @@ describe('$modal', function () {
body.find('div.modal').remove();
body.find('div.modal-backdrop').remove();
body.removeClass('modal-open');
+ $document.off('keydown');
});
- function open(modalOptions) {
- var modal = $modal.open(modalOptions);
- $rootScope.$digest();
+ function triggerKeyDown(element, keyCode, shiftKey) {
+ var e = $.Event('keydown');
+ e.srcElement = element[0];
+ e.which = keyCode;
+ e.shiftKey = shiftKey;
+ element.trigger(e);
+ }
+
+ function open(modalOptions, noFlush, noDigest) {
+ var modal = $uibModal.open(modalOptions);
+ modal.opened['catch'](angular.noop);
+ modal.result['catch'](angular.noop);
+
+ if (!noDigest) {
+ $rootScope.$digest();
+ if (!noFlush) {
+ $animate.flush();
+ }
+ }
+
return modal;
}
function close(modal, result, noFlush) {
var closed = modal.close(result);
+ $rootScope.$digest();
if (!noFlush) {
- $timeout.flush();
+ $animate.flush();
+ $rootScope.$digest();
+ $animate.flush();
+ $rootScope.$digest();
}
- $rootScope.$digest();
return closed;
}
function dismiss(modal, reason, noFlush) {
var closed = modal.dismiss(reason);
+ $rootScope.$digest();
if (!noFlush) {
- $timeout.flush();
+ $animate.flush();
+ $rootScope.$digest();
+ $animate.flush();
+ $rootScope.$digest();
}
- $rootScope.$digest();
return closed;
}
- describe('basic scenarios with default options', function () {
-
- it('should open and dismiss a modal with a minimal set of options', function () {
-
+ describe('basic scenarios with default options', function() {
+ it('should open and dismiss a modal with a minimal set of options', function() {
var modal = open({template: '
Content
'});
expect($document).toHaveModalsOpen(1);
@@ -180,8 +348,83 @@ describe('$modal', function () {
expect($document).not.toHaveBackdrop();
});
- it('should not throw an exception on a second dismiss', function () {
+ it('should compile modal before inserting into DOM', function() {
+ var topModal;
+ var modalInstance = {
+ result: $q.defer(),
+ opened: $q.defer(),
+ closed: $q.defer(),
+ rendered: $q.defer(),
+ close: function (result) {
+ return $uibModalStack.close(modalInstance, result);
+ },
+ dismiss: function (reason) {
+ return $uibModalStack.dismiss(modalInstance, reason);
+ }
+ };
+ var expectedText = 'test';
+
+ $uibModalStack.open(modalInstance, {
+ appendTo: angular.element(document.body),
+ scope: $rootScope.$new(),
+ deferred: modalInstance.result,
+ renderDeferred: modalInstance.rendered,
+ closedDeferred: modalInstance.closed,
+ content: '
'
+ });
+
+ $rootScope.$digest();
+ expect(rendered).toBe(false);
+
+ $animate.flush();
+
+ expect(rendered).toBe(true);
+ });
+
+ it('should not throw an exception on a second dismiss', function() {
var modal = open({template: '
Content
'});
expect($document).toHaveModalsOpen(1);
@@ -195,8 +438,7 @@ describe('$modal', function () {
dismiss(modal, 'closing in test', true);
});
- it('should not throw an exception on a second close', function () {
-
+ it('should not throw an exception on a second close', function() {
var modal = open({template: '
Content
'});
expect($document).toHaveModalsOpen(1);
@@ -210,8 +452,7 @@ describe('$modal', function () {
close(modal, 'closing in test', true);
});
- it('should open a modal from templateUrl', function () {
-
+ it('should open a modal from templateUrl', function() {
$templateCache.put('content.html', '
URL Content
');
var modal = open({templateUrl: 'content.html'});
@@ -226,31 +467,59 @@ describe('$modal', function () {
expect($document).not.toHaveBackdrop();
});
- it('should support closing on ESC', function () {
-
+ it('should support closing on ESC', function() {
var modal = open({template: '
Content
'});
expect($document).toHaveModalsOpen(1);
triggerKeyDown($document, 27);
- $timeout.flush();
+ $animate.flush();
+ $rootScope.$digest();
+ $animate.flush();
$rootScope.$digest();
expect($document).toHaveModalsOpen(0);
});
- it('should support closing on backdrop click', function () {
+ it('should not close on ESC if event.preventDefault() was issued', function() {
+ var modal = open({template: '
'});
expect($document).toHaveModalsOpen(1);
$document.find('body > div.modal').click();
- $timeout.flush();
+ $animate.flush();
+ $rootScope.$digest();
+ $animate.flush();
$rootScope.$digest();
expect($document).toHaveModalsOpen(0);
});
- it('should return to the element which had focus before the dialog is invoked', function () {
+ it('should return to the element which had focus before the dialog was invoked', function() {
var link = 'Link';
var element = angular.element(link);
angular.element(document.body).append(element);
@@ -258,12 +527,14 @@ describe('$modal', function () {
expect(document.activeElement.tagName).toBe('A');
var modal = open({template: '
Contentinside modal
'});
- $timeout.flush();
- expect(document.activeElement.tagName).toBe('DIV');
+ $rootScope.$digest();
+ expect(document.activeElement.className.split(' ')).toContain('modal');
expect($document).toHaveModalsOpen(1);
triggerKeyDown($document, 27);
- $timeout.flush();
+ $animate.flush();
+ $rootScope.$digest();
+ $animate.flush();
$rootScope.$digest();
expect(document.activeElement.tagName).toBe('A');
@@ -272,22 +543,89 @@ describe('$modal', function () {
element.remove();
});
- it('should resolve returned promise on close', function () {
+ it('should return to document.body if element which had focus before the dialog was invoked is gone, or is missing focus function', function() {
+ var link = 'Link';
+ var element = angular.element(link);
+ angular.element(document.body).append(element);
+ element.focus();
+ expect(document.activeElement.tagName).toBe('A');
+
+ var modal = open({template: '
Content
'});
+ $rootScope.$digest();
+ expect(document.activeElement.tagName).toBe('DIV');
+ expect($document).toHaveModalsOpen(1);
+
+ // Fake undefined focus function, happening in IE in certain
+ // iframe conditions. See issue 3639
+ element[0].focus = undefined;
+ triggerKeyDown($document, 27);
+ $animate.flush();
+ $rootScope.$digest();
+ $animate.flush();
+ $rootScope.$digest();
+
+ expect(document.activeElement.tagName).toBe('BODY');
+ expect($document).toHaveModalsOpen(0);
+ element.remove();
+ });
+
+ it('should resolve returned promise on close', function() {
var modal = open({template: '
Content
'});
close(modal, 'closed ok');
expect(modal.result).toBeResolvedWith('closed ok');
});
- it('should reject returned promise on dismiss', function () {
-
+ it('should reject returned promise on dismiss', function() {
var modal = open({template: '
Content
'});
dismiss(modal, 'esc');
expect(modal.result).toBeRejectedWith('esc');
});
- it('should expose a promise linked to the templateUrl / resolve promises', function () {
+ it('should reject returned promise on unexpected closure', function() {
+ var scope = $rootScope.$new();
+ var modal = open({template: '
Content
', scope: scope});
+ scope.$destroy();
+
+ expect(modal.result).toBeRejectedWith('$uibUnscheduledDestruction');
+
+ $animate.flush();
+ $rootScope.$digest();
+ $animate.flush();
+ $rootScope.$digest();
+ expect($document).toHaveModalsOpen(0);
+ });
+
+ it('should resolve the closed promise when modal is closed', function() {
+ var modal = open({template: '
Content
'});
+ var closed = false;
+ close(modal, 'closed ok');
+
+ modal.closed.then(function() {
+ closed = true;
+ });
+
+ $rootScope.$digest();
+
+ expect(closed).toBe(true);
+ });
+
+ it('should resolve the closed promise when modal is dismissed', function() {
+ var modal = open({template: '
Content
'});
+ var closed = false;
+ dismiss(modal, 'esc');
+
+ modal.closed.then(function() {
+ closed = true;
+ });
+
+ $rootScope.$digest();
+
+ expect(closed).toBe(true);
+ });
+
+ it('should expose a promise linked to the templateUrl / resolve promises', function() {
var modal = open({template: '
Content
', resolve: {
ok: function() {return $q.when('ok');}
}}
@@ -295,18 +633,17 @@ describe('$modal', function () {
expect(modal.opened).toBeResolvedWith(true);
});
- it('should expose a promise linked to the templateUrl / resolve promises and reject it if needed', function () {
+ it('should expose a promise linked to the templateUrl / resolve promises and reject it if needed', function() {
var modal = open({template: '
Content
', resolve: {
- ok: function() {return $q.reject('ko');}
- }}
- );
+ ok: function() {return $q.reject('ko');}
+ }}, true);
expect(modal.opened).toBeRejectedWith('ko');
});
- it('should focus on the element that has autofocus attribute when the modal is open/reopen', function () {
+ it('should focus on the element that has autofocus attribute when the modal is open/reopen and the animations have finished', function() {
function openAndCloseModalWithAutofocusElement() {
var modal = open({template: ''});
-
+ $rootScope.$digest();
expect(angular.element('#auto-focus-element')).toHaveFocus();
close(modal, 'closed ok');
@@ -317,22 +654,301 @@ describe('$modal', function () {
openAndCloseModalWithAutofocusElement();
openAndCloseModalWithAutofocusElement();
});
- });
- describe('default options can be changed in a provider', function () {
+ it('should not focus on the element that has autofocus attribute when the modal is opened and something in the modal already has focus and the animations have finished', function() {
+ function openAndCloseModalWithAutofocusElement() {
+
+ var modal = open({template: ''});
+ $rootScope.$digest();
+ expect(angular.element('#auto-focus-element')).not.toHaveFocus();
+ expect(angular.element('#pre-focus-element')).toHaveFocus();
+
+ close(modal, 'closed ok');
+
+ expect(modal.result).toBeResolvedWith('closed ok');
+ }
+
+ openAndCloseModalWithAutofocusElement();
+ openAndCloseModalWithAutofocusElement();
+ });
+
+ it('should wait until the in animation is finished before attempting to focus the modal or autofocus element', function() {
+ function openAndCloseModalWithAutofocusElement() {
+ var modal = open({template: ''}, true, true);
+ expect(angular.element('#auto-focus-element')).not.toHaveFocus();
+
+ $rootScope.$digest();
+ $animate.flush();
+
+ expect(angular.element('#auto-focus-element')).toHaveFocus();
+
+ close(modal, 'closed ok');
+
+ expect(modal.result).toBeResolvedWith('closed ok');
+ }
+
+ function openAndCloseModalWithOutAutofocusElement() {
+ var link = 'Link';
+ var element = angular.element(link);
+ angular.element(document.body).append(element);
+ element.focus();
+ expect(document.activeElement.tagName).toBe('A');
+
+ var modal = open({template: ''}, true, true);
+ expect(document.activeElement.tagName).toBe('A');
+
+ $rootScope.$digest();
+ $animate.flush();
+
+ expect(document.activeElement.className.split(' ')).toContain('modal');
+
+ close(modal, 'closed ok');
+
+ expect(modal.result).toBeResolvedWith('closed ok');
+
+ element.remove();
+ }
+
+ openAndCloseModalWithAutofocusElement();
+ openAndCloseModalWithOutAutofocusElement();
+ });
+
+ it('should change focus to first element when tab key was pressed', function() {
+ var initialPage = angular.element('Outland link');
+ angular.element(document.body).append(initialPage);
+ initialPage.focus();
+
+ open({
+ template:'' +
+ 'Open me!'
+ });
+ expect($document).toHaveModalsOpen(1);
+
+ var lastElement = angular.element(document.getElementById('tab-focus-button'));
+ lastElement.focus();
+ triggerKeyDown(lastElement, 9);
+ expect(document.activeElement.getAttribute('id')).toBe('tab-focus-link');
+
+ initialPage.remove();
+ });
+
+ it('should change focus to last element when shift+tab key is pressed', function() {
+ var initialPage = angular.element('Outland link');
+ angular.element(document.body).append(initialPage);
+ initialPage.focus();
+
+ open({
+ template:'' +
+ 'Open me!'
+ });
+ $rootScope.$digest();
+ expect($document).toHaveModalsOpen(1);
+
+ triggerKeyDown(angular.element(document.activeElement), 9, true);
+ expect(document.activeElement.getAttribute('id')).toBe('tab-focus-button');
+
+ var lastElement = angular.element(document.getElementById('tab-focus-link'));
+ lastElement.focus();
+ triggerKeyDown(angular.element(document.activeElement), 9, true);
+ expect(document.activeElement.getAttribute('id')).toBe('tab-focus-button');
+
+ initialPage.remove();
+ });
+
+ it('should change focus to first element when tab key is pressed when keyboard is false', function() {
+ var initialPage = angular.element('Outland link');
+ angular.element(document.body).append(initialPage);
+ initialPage.focus();
+
+ open({
+ template:'' +
+ 'Open me!',
+ keyboard: false
+ });
+ expect($document).toHaveModalsOpen(1);
+
+ var lastElement = angular.element(document.getElementById('tab-focus-button'));
+ lastElement.focus();
+ triggerKeyDown(lastElement, 9);
+ expect(document.activeElement.getAttribute('id')).toBe('tab-focus-link');
+
+ initialPage.remove();
+ });
+
+ it('should change focus to last element when shift+tab keys are pressed when keyboard is false', function() {
+ var initialPage = angular.element('Outland link');
+ angular.element(document.body).append(initialPage);
+ initialPage.focus();
+
+ open({
+ template:'' +
+ 'Open me!',
+ keyboard: false
+ });
+ $rootScope.$digest();
+ expect($document).toHaveModalsOpen(1);
+
+ triggerKeyDown(angular.element(document.activeElement), 9, true);
+ expect(document.activeElement.getAttribute('id')).toBe('tab-focus-button');
+
+ var lastElement = angular.element(document.getElementById('tab-focus-link'));
+ lastElement.focus();
+ triggerKeyDown(angular.element(document.activeElement), 9, true);
+ expect(document.activeElement.getAttribute('id')).toBe('tab-focus-button');
+
+ initialPage.remove();
+ });
+
+ it('should change focus to next proper element when DOM changes and tab is pressed', function() {
+ var initialPage = angular.element('Outland link');
+ angular.element(document.body).append(initialPage);
+ initialPage.focus();
+
+ open({
+ template:'abc' +
+ 'Open me!',
+ keyboard: false
+ });
+ $rootScope.$digest();
+ expect($document).toHaveModalsOpen(1);
+
+ $('#tab-focus-link3').focus();
+ expect(document.activeElement.getAttribute('id')).toBe('tab-focus-link3');
+
+ $('#tab-focus-button').remove();
+ triggerKeyDown(angular.element(document.activeElement), 9, false);
+ expect(document.activeElement.getAttribute('id')).toBe('tab-focus-link1');
+
+ initialPage.remove();
+ });
+
+ it('should change focus to next proper element when DOM changes and shift+tab is pressed', function() {
+ var initialPage = angular.element('Outland link');
+ angular.element(document.body).append(initialPage);
+ initialPage.focus();
+
+ open({
+ template:'abc' +
+ 'Open me!',
+ keyboard: false
+ });
+ $rootScope.$digest();
+ expect($document).toHaveModalsOpen(1);
+
+ $('#tab-focus-link1').focus();
+ expect(document.activeElement.getAttribute('id')).toBe('tab-focus-link1');
+
+ $('#tab-focus-button').remove();
+ triggerKeyDown(angular.element(document.activeElement), 9, true);
+ expect(document.activeElement.getAttribute('id')).toBe('tab-focus-link3');
+
+ initialPage.remove();
+ });
+
+ it('should change focus to next non-hidden element when tab is pressed', function() {
+ var initialPage = angular.element('Outland link');
+ angular.element(document.body).append(initialPage);
+ initialPage.focus();
+
+ open({
+ template:'abc' +
+ 'Open me!',
+ keyboard: false
+ });
+ $rootScope.$digest();
+ expect($document).toHaveModalsOpen(1);
+
+ $('#tab-focus-link3').focus();
+ expect(document.activeElement.getAttribute('id')).toBe('tab-focus-link3');
+
+ $('#tab-focus-button').css('display', 'none');
+ triggerKeyDown(angular.element(document.activeElement), 9, false);
+ expect(document.activeElement.getAttribute('id')).toBe('tab-focus-link1');
+
+ initialPage.remove();
+ });
+
+ it('should change focus to previous non-hidden element when shift+tab is pressed', function() {
+ var initialPage = angular.element('Outland link');
+ angular.element(document.body).append(initialPage);
+ initialPage.focus();
+
+ open({
+ template:'abc' +
+ 'Open me!',
+ keyboard: false
+ });
+ $rootScope.$digest();
+ expect($document).toHaveModalsOpen(1);
+
+ $('#tab-focus-link1').focus();
+ expect(document.activeElement.getAttribute('id')).toBe('tab-focus-link1');
- it('should allow overriding default options in a provider', function () {
+ $('#tab-focus-button').css('display', 'none');
+ triggerKeyDown(angular.element(document.activeElement), 9, true);
+ expect(document.activeElement.getAttribute('id')).toBe('tab-focus-link3');
- $modalProvider.options.backdrop = false;
+ initialPage.remove();
+ });
+
+ it('should change focus to next tabbable element when tab is pressed', function() {
+ var initialPage = angular.element('Outland link');
+ angular.element(document.body).append(initialPage);
+ initialPage.focus();
+
+ open({
+ template:'Skip me!a' +
+ 'bc' +
+ 'Skip me!',
+ keyboard: false
+ });
+ $rootScope.$digest();
+ expect($document).toHaveModalsOpen(1);
+
+ $('#tab-focus-link3').focus();
+ expect(document.activeElement.getAttribute('id')).toBe('tab-focus-link3');
+
+ triggerKeyDown(angular.element(document.activeElement), 9, false);
+ expect(document.activeElement.getAttribute('id')).toBe('tab-focus-link1');
+
+ initialPage.remove();
+ });
+
+ it('should change focus to previous tabbable element when shift+tab is pressed', function() {
+ var initialPage = angular.element('Outland link');
+ angular.element(document.body).append(initialPage);
+ initialPage.focus();
+
+ open({
+ template:'Skip me!a' +
+ 'bc' +
+ 'Skip me!',
+ keyboard: false
+ });
+ $rootScope.$digest();
+ expect($document).toHaveModalsOpen(1);
+
+ $('#tab-focus-link1').focus();
+ expect(document.activeElement.getAttribute('id')).toBe('tab-focus-link1');
+
+ triggerKeyDown(angular.element(document.activeElement), 9, true);
+ expect(document.activeElement.getAttribute('id')).toBe('tab-focus-link3');
+
+ initialPage.remove();
+ });
+ });
+
+ describe('default options can be changed in a provider', function() {
+ it('should allow overriding default options in a provider', function() {
+ $uibModalProvider.options.backdrop = false;
var modal = open({template: '
Content
'});
expect($document).toHaveModalOpenWithContent('Content', 'div');
expect($document).not.toHaveBackdrop();
});
- it('should accept new objects with default options in a provider', function () {
-
- $modalProvider.options = {
+ it('should accept new objects with default options in a provider', function() {
+ $uibModalProvider.options = {
backdrop: false
};
var modal = open({template: '
Content
'});
@@ -342,24 +958,95 @@ describe('$modal', function () {
});
});
- describe('option by option', function () {
+ describe('option by option', function() {
+ describe('component', function() {
+ function getModalComponent($document) {
+ return $document.find('body > div.modal > div.modal-dialog > div.modal-content foo-bar');
+ }
+
+ it('should use as modal content', function() {
+ open({
+ component: 'fooBar'
+ });
+
+ var component = getModalComponent($document);
+ expect(component.html()).toBe('
Foo Bar
');
+ });
+
+ it('should bind expected values', function() {
+ var modal = open({
+ component: 'fooBar',
+ resolve: {
+ foo: function() {
+ return 'bar';
+ }
+ }
+ });
+
+ var component = getModalComponent($document);
+ var componentScope = component.isolateScope();
- describe('template and templateUrl', function () {
+ expect(componentScope.foobar.resolve.foo).toBe('bar');
+ expect(componentScope.foobar.modalInstance).toBe(modal);
+ expect(componentScope.foobar.close).toEqual(jasmine.any(Function));
+ expect(componentScope.foobar.dismiss).toEqual(jasmine.any(Function));
+ });
+
+ it('should close the modal', function() {
+ var modal = open({
+ component: 'fooBar',
+ resolve: {
+ foo: function() {
+ return 'bar';
+ }
+ }
+ });
+
+ var component = getModalComponent($document);
+ var componentScope = component.isolateScope();
- it('should throw an error if none of template and templateUrl are provided', function () {
+ componentScope.foobar.close({
+ $value: 'baz'
+ });
+
+ expect(modal.result).toBeResolvedWith('baz');
+ });
+
+ it('should dismiss the modal', function() {
+ var modal = open({
+ component: 'fooBar',
+ resolve: {
+ foo: function() {
+ return 'bar';
+ }
+ }
+ });
+
+ var component = getModalComponent($document);
+ var componentScope = component.isolateScope();
+
+ componentScope.foobar.dismiss({
+ $value: 'baz'
+ });
+
+ expect(modal.result).toBeRejectedWith('baz');
+ });
+ });
+
+ describe('template and templateUrl', function() {
+ it('should throw an error if none of component, template and templateUrl are provided', function() {
expect(function(){
var modal = open({});
- }).toThrow(new Error('One of template or templateUrl options is required.'));
+ }).toThrow(new Error('One of component or template or templateUrl options is required.'));
});
- it('should not fail if a templateUrl contains leading / trailing white spaces', function () {
-
+ it('should not fail if a templateUrl contains leading / trailing white spaces', function() {
$templateCache.put('whitespace.html', '
Whitespaces
');
open({templateUrl: 'whitespace.html'});
expect($document).toHaveModalOpenWithContent('Whitespaces', 'div');
});
- it('should accept template as a function', function () {
+ it('should accept template as a function', function() {
open({template: function() {
return '
From a function
';
}});
@@ -367,60 +1054,97 @@ describe('$modal', function () {
expect($document).toHaveModalOpenWithContent('From a function', 'div');
});
- it('should not fail if a templateUrl as a function', function () {
+ it('should not fail if a templateUrl as a function', function() {
$templateCache.put('whitespace.html', '
', 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) {
+ it('should respect the controllerAs property as an alternative for the controller-as syntax', function() {
+ $controllerProvider.register('TestCtrl', function($uibModalInstance) {
this.fromCtrl = 'Content from ctrl';
- this.isModalInstance = angular.isObject($modalInstance) && angular.isFunction($modalInstance.close);
+ this.isModalInstance = angular.isObject($uibModalInstance) && angular.isFunction($uibModalInstance.close);
});
open({template: '
'
+ });
+ });
+ it('should delay showing modal if one of the resolves is a promise', function() {
open(modalDefinition('
{{value}}
', {
- value: function () {
- return $timeout(function(){ return 'Promise'; }, 100);
+ value: function() {
+ return $timeout(function() { return 'Promise'; }, 100);
}
- }));
+ }), true);
expect($document).toHaveModalsOpen(0);
$timeout.flush();
expect($document).toHaveModalOpenWithContent('Promise', 'div');
});
- it('should not open dialog (and reject returned promise) if one of resolve fails', function () {
-
+ it('should not open dialog (and reject returned promise) if one of resolve fails', function() {
var deferred = $q.defer();
var modal = open(modalDefinition('
{{value}}
', {
- value: function () {
+ value: function() {
return deferred.promise;
}
- }));
+ }), true);
expect($document).toHaveModalsOpen(0);
deferred.reject('error in test');
@@ -474,23 +1224,19 @@ describe('$modal', function () {
expect(modal.result).toBeRejectedWith('error in test');
});
- it('should support injection with minification-safe syntax in resolve functions', function () {
-
+ it('should support injection with minification-safe syntax in resolve functions', function() {
open(modalDefinition('
{{value.id}}
', {
- value: ['$locale', function (e) {
+ 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 () {
+ describe('scope', function() {
+ it('should use custom scope if provided', function() {
var $scope = $rootScope.$new();
$scope.fromScope = 'Content from custom scope';
open({
@@ -500,8 +1246,7 @@ describe('$modal', function () {
expect($document).toHaveModalOpenWithContent('Content from custom scope', 'div');
});
- it('should create and use child of $rootScope if custom scope not provided', function () {
-
+ it('should create and use child of $rootScope if custom scope not provided', function() {
var scopeTailBefore = $rootScope.$$childTail;
$rootScope.fromScope = 'Content from root scope';
@@ -510,11 +1255,24 @@ describe('$modal', function () {
});
expect($document).toHaveModalOpenWithContent('Content from root scope', 'div');
});
+
+ it('should expose $resolve in template', function() {
+ open({
+ controller: function($scope) {},
+ resolve: {
+ $foo: function() {
+ return 'Content from resolve';
+ }
+ },
+ template: '
{{$resolve.$foo}}
'
+ });
+
+ expect($document).toHaveModalOpenWithContent('Content from resolve', 'div');
+ });
});
describe('keyboard', function () {
-
- it('should not close modals if keyboard option is set to false', function () {
+ it('should not close modals if keyboard option is set to false', function() {
open({
template: '
No keyboard
',
keyboard: false
@@ -529,10 +1287,9 @@ describe('$modal', function () {
});
});
- describe('backdrop', function () {
-
- it('should not have any backdrop element if backdrop set to false', function () {
- var modal =open({
+ describe('backdrop', function() {
+ it('should not have any backdrop element if backdrop set to false', function() {
+ var modal = open({
template: '
No backdrop
',
backdrop: false
});
@@ -543,7 +1300,7 @@ describe('$modal', function () {
expect($document).toHaveModalsOpen(0);
});
- it('should not close modal on backdrop click if backdrop is specified as "static"', function () {
+ it('should not close modal on backdrop click if backdrop is specified as "static"', function() {
open({
template: '
Static backdrop
',
backdrop: 'static'
@@ -556,26 +1313,21 @@ describe('$modal', function () {
expect($document).toHaveBackdrop();
});
- it('should animate backdrop on each modal opening', function () {
-
+ it('should contain backdrop in classes on each modal opening', function() {
var modal = open({ template: '
' });
backdropEl = $document.find('body > div.modal-backdrop');
- expect(backdropEl).not.toHaveClass('in');
+ expect(backdropEl).toHaveClass('in');
});
describe('custom backdrop classes', function () {
-
- it('should support additional backdrop class as string', function () {
+ it('should support additional backdrop class as string', function() {
open({
template: '
With custom backdrop class
',
backdropClass: 'additional'
@@ -586,9 +1338,8 @@ describe('$modal', function () {
});
});
- describe('custom window classes', function () {
-
- it('should support additional window class as string', function () {
+ describe('custom window classes', function() {
+ it('should support additional window class as string', function() {
open({
template: '
With custom window class
',
windowClass: 'additional'
@@ -598,9 +1349,19 @@ describe('$modal', function () {
});
});
- describe('size', function () {
+ describe('top window class', function () {
+ it('should support top class option', function () {
+ open({
+ template: '
With custom window top class
',
+ windowTopClass: 'top-class'
+ });
+
+ expect($document.find('div.modal')).toHaveClass('top-class');
+ });
+ });
- it('should support creating small modal dialogs', function () {
+ describe('size', function() {
+ it('should support creating small modal dialogs', function() {
open({
template: '
Small modal dialog
',
size: 'sm'
@@ -609,7 +1370,7 @@ describe('$modal', function () {
expect($document.find('div.modal-dialog')).toHaveClass('modal-sm');
});
- it('should support creating large modal dialogs', function () {
+ it('should support creating large modal dialogs', function() {
open({
template: '
Large modal dialog
',
size: 'lg'
@@ -618,7 +1379,7 @@ describe('$modal', function () {
expect($document.find('div.modal-dialog')).toHaveClass('modal-lg');
});
- it('should support custom size modal dialogs', function () {
+ it('should support custom size modal dialogs', function() {
open({
template: '
Large modal dialog
',
size: 'custom'
@@ -628,18 +1389,17 @@ describe('$modal', function () {
});
});
- describe('animation', function () {
-
- it('should have animation fade classes by default', function () {
+ describe('animation', function() {
+ it('should have animation fade classes by default', function() {
open({
- template: '
Small modal dialog
',
+ template: '
Small modal dialog
'
});
expect($document.find('.modal')).toHaveClass('fade');
expect($document.find('.modal-backdrop')).toHaveClass('fade');
});
- it('should not have fade classes if animation false', function () {
+ it('should not have fade classes if animation false', function() {
open({
template: '
Small modal dialog
',
animation: false
@@ -648,15 +1408,218 @@ describe('$modal', function () {
expect($document.find('.modal')).not.toHaveClass('fade');
expect($document.find('.modal-backdrop')).not.toHaveClass('fade');
});
+ });
+
+ describe('appendTo', function() {
+ it('should be added to body by default', function() {
+ var modal = open({template: '
Content
'});
+
+ expect($document).toHaveModalsOpen(1);
+ expect($document).toHaveModalOpenWithContent('Content', 'div');
+ });
+
+ it('should not be added to body if appendTo is passed', function() {
+ var element = angular.element('Some content');
+ angular.element(document.body).append(element);
+
+ var modal = open({template: '
Content
', appendTo: element});
+
+ expect($document).not.toHaveModalOpenWithContent('Content', 'div');
+
+ element.remove();
+ });
+
+ it('should be added to appendTo element if appendTo is passed', function() {
+ var element = angular.element('Some content');
+ angular.element(document.body).append(element);
+
+ expect($document.find('section').children('div.modal').length).toBe(0);
+ open({template: '
Content
', appendTo: element});
+ expect($document.find('section').children('div.modal').length).toBe(1);
+
+ element.remove();
+ });
+
+ it('should throw error if appendTo element is not found', function() {
+ expect(function(){
+ open({template: '
Content
', appendTo: $document.find('aside')});
+ }).toThrow(new Error('appendTo element not found. Make sure that the element passed is in DOM.'));
+ });
+
+ it('should be removed from appendTo element when dismissed', function() {
+ var modal = open({template: '
'});
expect($document).toHaveModalsOpen(2);
@@ -669,8 +1632,18 @@ describe('$modal', function () {
expect($document).toHaveModalsOpen(0);
});
- it('should not close any modals on ESC if the topmost one does not allow it', function () {
+ it('should be able to dismiss all modals at once', function() {
+ var modal1 = open({template: '
Modal1
'});
+ var modal2 = open({template: '
Modal2
'});
+ expect($document).toHaveModalsOpen(2);
+
+ $uibModalStack.dismissAll();
+ $animate.flush();
+ $animate.flush();
+ expect($document).toHaveModalsOpen(0);
+ });
+ it('should not close any modals on ESC if the topmost one does not allow it', function() {
var modal1 = open({template: '
Modal1
'});
var modal2 = open({template: '
Modal2
', keyboard: false});
@@ -680,8 +1653,7 @@ describe('$modal', function () {
expect($document).toHaveModalsOpen(2);
});
- it('should not close any modals on click if a topmost modal does not have backdrop', function () {
-
+ it('should not close any modals on click if a topmost modal does not have backdrop', function() {
var modal1 = open({template: '
Modal1
'});
var modal2 = open({template: '
Modal2
', backdrop: false});
@@ -691,8 +1663,7 @@ describe('$modal', function () {
expect($document).toHaveModalsOpen(2);
});
- it('multiple modals should not interfere with default options', function () {
-
+ it('should not interfere with default options', function() {
var modal1 = open({template: '
Modal1
', backdrop: false});
var modal2 = open({template: '
Modal2
'});
$rootScope.$digest();
@@ -700,8 +1671,7 @@ describe('$modal', function () {
expect($document).toHaveBackdrop();
});
- it('should add "modal-open" class when a modal gets opened', function () {
-
+ it('should add "modal-open" class when a modal gets opened', function() {
var body = $document.find('body');
expect(body).not.toHaveClass('modal-open');
@@ -718,7 +1688,7 @@ describe('$modal', function () {
expect(body).not.toHaveClass('modal-open');
});
- it('should return to the element which had focus before the dialog is invoked', function () {
+ it('should return to the element which had focus before the dialog is invoked', function() {
var link = 'Link';
var element = angular.element(link);
angular.element(document.body).append(element);
@@ -726,13 +1696,13 @@ describe('$modal', function () {
expect(document.activeElement.tagName).toBe('A');
var modal1 = open({template: '
'});
- $timeout.flush();
+ $rootScope.$digest();
expect(document.activeElement.tagName).toBe('DIV');
expect($document).toHaveModalsOpen(2);
@@ -746,6 +1716,338 @@ describe('$modal', function () {
element.remove();
});
+
+ it('should open modals and resolve the opened promises in order', function() {
+ // Opens a modal for each element in array order.
+ // Order is an array of non-repeating integers from 0..length-1 representing when to resolve that modal's promise.
+ // For example [1,2,0] would resolve the 3rd modal's promise first and the 2nd modal's promise last.
+ // Tests that the modals are added to $uibModalStack and that each resolves its "opened" promise sequentially.
+ // If an element is {reject:n} then n is still the order, but the corresponding promise is rejected.
+ // A rejection earlier in the open sequence should not affect modals opened later.
+ function test(order) {
+ var ds = []; // {index, deferred, reject}
+ var expected = ''; // 0..length-1
+ var actual = '';
+ angular.forEach(order, function(x, i) {
+ var reject = x.reject !== undefined;
+ if (reject) {
+ x = x.reject;
+ } else {
+ expected += i;
+ }
+ ds[x] = {index: i, deferred: $q.defer(), reject: reject};
+
+ var scope = $rootScope.$new();
+ var failed = false;
+ scope.index = i;
+ open({
+ template: '
');
+ });
+
it('disables the "previous" link if current page is 1', function() {
updateCurrentPage(1);
expect(getPaginationEl(0)).toHaveClass('disabled');
@@ -85,7 +102,7 @@ describe('pager directive', function () {
it('executes the `ng-change` expression when an element is clicked', function() {
$rootScope.selectPageHandler = jasmine.createSpy('selectPageHandler');
- element = $compile('')($rootScope);
+ element = $compile('
')($rootScope);
$rootScope.$digest();
clickPaginationEl(-1);
@@ -101,21 +118,21 @@ describe('pager directive', function () {
expect(getPaginationEl(-1).text()).toBe('Next »');
});
- it('should blur the "next" link after it has been clicked', function () {
- $document.find('body').append(element);
+ it('should blur the "next" link after it has been clicked', function() {
+ body.append(element);
var linkEl = getPaginationLinkEl(element, -1);
-
+
linkEl.focus();
expect(linkEl).toHaveFocus();
-
+
linkEl.click();
expect(linkEl).not.toHaveFocus();
-
+
element.remove();
});
- it('should blur the "prev" link after it has been clicked', function () {
- $document.find('body').append(element);
+ it('should blur the "prev" link after it has been clicked', function() {
+ body.append(element);
var linkEl = getPaginationLinkEl(element, -1);
linkEl.focus();
@@ -126,11 +143,20 @@ describe('pager directive', function () {
element.remove();
});
-
- describe('`items-per-page`', function () {
+
+ it('allows custom templates', function() {
+ $templateCache.put('foo/bar.html', '
');
+ });
+
it('contains num-pages + 2 li elements', function() {
expect(getPaginationBarSize()).toBe(7);
expect(getPaginationEl(0).text()).toBe('Previous');
@@ -127,49 +180,49 @@ describe('pagination directive', function () {
expect($rootScope.currentPage).toBe(1);
});
- it('should blur a page link after it has been clicked', function () {
- $document.find('body').append(element);
+ it('should blur a page link after it has been clicked', function() {
+ body.append(element);
var linkEl = getPaginationLinkEl(element, 2);
-
+
linkEl.focus();
expect(linkEl).toHaveFocus();
-
+
linkEl.click();
expect(linkEl).not.toHaveFocus();
-
+
element.remove();
});
-
- it('should blur the "next" link after it has been clicked', function () {
- $document.find('body').append(element);
+
+ it('should blur the "next" link after it has been clicked', function() {
+ body.append(element);
var linkEl = getPaginationLinkEl(element, -1);
-
+
linkEl.focus();
expect(linkEl).toHaveFocus();
-
+
linkEl.click();
expect(linkEl).not.toHaveFocus();
-
+
element.remove();
});
-
- it('should blur the "prev" link after it has been clicked', function () {
- $document.find('body').append(element);
+
+ it('should blur the "prev" link after it has been clicked', function() {
+ body.append(element);
var linkEl = getPaginationLinkEl(element, 0);
-
+
linkEl.focus();
expect(linkEl).toHaveFocus();
-
+
linkEl.click();
expect(linkEl).not.toHaveFocus();
-
+
element.remove();
});
-
- describe('`items-per-page`', function () {
+
+ describe('`items-per-page`', function() {
beforeEach(function() {
$rootScope.perpage = 5;
- element = $compile('')($rootScope);
+ element = $compile('
')($rootScope);
$rootScope.$digest();
});
@@ -209,10 +262,10 @@ describe('pagination directive', function () {
});
});
- describe('executes `ng-change` expression', function () {
+ describe('executes `ng-change` expression', function() {
beforeEach(function() {
$rootScope.selectPageHandler = jasmine.createSpy('selectPageHandler');
- element = $compile('')($rootScope);
+ element = $compile('
')($rootScope);
$rootScope.$digest();
});
@@ -222,7 +275,7 @@ describe('pagination directive', function () {
});
});
- describe('when `page` is not a number', function () {
+ describe('when `page` is not a number', function() {
it('handles numerical string', function() {
updateCurrentPage('2');
expect(getPaginationEl(2)).toHaveClass('active');
@@ -237,12 +290,12 @@ describe('pagination directive', function () {
});
});
- describe('with `max-size` option', function () {
+ describe('with `max-size` option', function() {
beforeEach(function() {
$rootScope.total = 98; // 10 pages
$rootScope.currentPage = 3;
$rootScope.maxSize = 5;
- element = $compile('')($rootScope);
+ element = $compile('
')($rootScope);
$rootScope.$digest();
});
@@ -303,28 +356,182 @@ describe('pagination directive', function () {
expect(getPaginationEl(0).text()).toBe('Previous');
expect(getPaginationEl(-1).text()).toBe('Next');
});
-
+
it('should blur page link when visible range changes', function () {
- $document.find('body').append(element);
+ body.append(element);
var linkEl = getPaginationLinkEl(element, 4);
-
+
linkEl.focus();
expect(linkEl).toHaveFocus();
-
+
linkEl.click();
expect(linkEl).not.toHaveFocus();
-
+
element.remove();
});
});
- describe('with `max-size` option & no `rotate`', function () {
+ describe('with `force-ellipses` option', function() {
+ beforeEach(function() {
+ $rootScope.total = 98; // 10 pages
+ $rootScope.currentPage = 3;
+ $rootScope.maxSize = 5;
+ element = $compile('
')($rootScope);
+ $rootScope.$digest();
+ });
+
+ it('contains maxsize + 3 li elements', function() {
+ expect(getPaginationBarSize()).toBe($rootScope.maxSize + 3);
+ expect(getPaginationEl(0).text()).toBe('Previous');
+ expect(getPaginationEl(-1).text()).toBe('Next');
+ expect(getPaginationEl(-2).text()).toBe('...');
+ });
+
+ it('shows the page number in middle after the next link is clicked', function() {
+ updateCurrentPage(6);
+ clickPaginationEl(-1);
+
+ expect($rootScope.currentPage).toBe(7);
+ expect(getPaginationEl(4)).toHaveClass('active');
+ expect(getPaginationEl(4).text()).toBe(''+$rootScope.currentPage);
+ });
+
+ it('shows the page number in middle after the prev link is clicked', function() {
+ updateCurrentPage(7);
+ clickPaginationEl(0);
+
+ expect($rootScope.currentPage).toBe(6);
+ expect(getPaginationEl(4)).toHaveClass('active');
+ expect(getPaginationEl(4).text()).toBe(''+$rootScope.currentPage);
+ });
+
+ it('changes pagination bar size when max-size value changed', function() {
+ $rootScope.maxSize = 7;
+ $rootScope.$digest();
+ expect(getPaginationBarSize()).toBe(10);
+ });
+
+ it('should display an ellipsis on the right if the last displayed page\'s number is less than the last page', function() {
+ updateCurrentPage(1);
+ expect(getPaginationAsText()).toBe('Previous, 1, 2, 3, 4, 5, ..., Next');
+ });
+
+ it('should display an ellipsis on the left if the first displayed page\'s number is greater than 1', function() {
+ updateCurrentPage(10);
+ expect(getPaginationAsText()).toBe('Previous, ..., 6, 7, 8, 9, 10, Next');
+ });
+
+ it('should display both ellipsis\' if the displayed range is in the middle', function() {
+ updateCurrentPage(5);
+ expect(getPaginationAsText()).toBe('Previous, ..., 3, 4, 5, 6, 7, ..., Next');
+ });
+
+ it('should not display any ellipses if the number of pages >= maxsize', function() {
+ $rootScope.maxSize = 10;
+ $rootScope.$digest();
+ expect(getPaginationAsText()).toBe('Previous, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, Next');
+ });
+ });
+
+ describe('with `boundary-link-numbers` option', function() {
+ beforeEach(function() {
+ $rootScope.total = 98; // 10 pages
+ $rootScope.currentPage = 3;
+ $rootScope.maxSize = 5;
+ element = $compile('
')($rootScope);
+ $rootScope.$digest();
+ });
+
+ it('contains maxsize + 4 li elements', function() {
+ expect(getPaginationBarSize()).toBe($rootScope.maxSize + 4);
+ expect(getPaginationEl(0).text()).toBe('Previous');
+ expect(getPaginationEl(-1).text()).toBe('Next');
+ expect(getPaginationEl(-2).text()).toBe('10');
+ expect(getPaginationEl(-3).text()).toBe('...');
+ });
+
+ it('shows the page number in middle after the next link is clicked', function() {
+ updateCurrentPage(6);
+ clickPaginationEl(-1);
+
+ expect($rootScope.currentPage).toBe(7);
+ expect(getPaginationEl(5)).toHaveClass('active');
+ expect(getPaginationEl(5).text()).toBe(''+$rootScope.currentPage);
+ });
+
+ it('shows the page number in middle after the prev link is clicked', function() {
+ updateCurrentPage(7);
+ clickPaginationEl(0);
+
+ expect($rootScope.currentPage).toBe(6);
+ expect(getPaginationEl(5)).toHaveClass('active');
+ expect(getPaginationEl(5).text()).toBe(''+$rootScope.currentPage);
+ });
+
+ it('changes pagination bar size when max-size value changed', function() {
+ $rootScope.maxSize = 7;
+ $rootScope.$digest();
+ expect(getPaginationBarSize()).toBe(11);
+ });
+
+ it('should display an ellipsis on the right if the last displayed page\'s number is less than the last page', function() {
+ updateCurrentPage(1);
+ expect(getPaginationAsText()).toBe('Previous, 1, 2, 3, 4, 5, ..., 10, Next');
+ });
+
+ it('should display an ellipsis on the left if the first displayed page\'s number is greater than 1', function() {
+ updateCurrentPage(10);
+ expect(getPaginationAsText()).toBe('Previous, 1, ..., 6, 7, 8, 9, 10, Next');
+ });
+
+ it('should display both ellipses if the displayed range is in the middle', function() {
+ $rootScope.maxSize = 3;
+ $rootScope.$digest();
+ updateCurrentPage(6);
+ expect(getPaginationAsText()).toBe('Previous, 1, ..., 5, 6, 7, ..., 10, Next');
+ });
+
+ it('should not display any ellipses if the number of pages >= maxsize', function() {
+ $rootScope.maxSize = 10;
+ $rootScope.$digest();
+ expect(getPaginationAsText()).toBe('Previous, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, Next');
+ });
+
+ it('should not display an ellipsis on the left if the start page is 2', function() {
+ updateCurrentPage(4);
+ expect(getPaginationAsText()).toBe('Previous, 1, 2, 3, 4, 5, 6, ..., 10, Next');
+ });
+
+ it('should not display an ellipsis on the left if the start page is 3', function() {
+ updateCurrentPage(5);
+ expect(getPaginationAsText()).toBe('Previous, 1, 2, 3, 4, 5, 6, 7, ..., 10, Next');
+ });
+
+ it('should not display an ellipsis on the right if the end page is totalPages - 1', function() {
+ updateCurrentPage(7);
+ expect(getPaginationAsText()).toBe('Previous, 1, ..., 5, 6, 7, 8, 9, 10, Next');
+ });
+
+ it('should not display an ellipsis on the right if the end page is totalPages - 2', function() {
+ updateCurrentPage(6);
+ expect(getPaginationAsText()).toBe('Previous, 1, ..., 4, 5, 6, 7, 8, 9, 10, Next');
+ });
+
+ it('should not display any ellipses if the number of pages <= maxsize + 4 and current page is in center', function() {
+ $rootScope.total = 88; // 9 pages
+ $rootScope.$digest();
+ updateCurrentPage(5);
+ expect(getPaginationAsText()).toBe('Previous, 1, 2, 3, 4, 5, 6, 7, 8, 9, Next');
+ });
+ });
+
+ describe('with `max-size` option & no `rotate`', function() {
beforeEach(function() {
$rootScope.total = 115; // 12 pages
$rootScope.currentPage = 7;
$rootScope.maxSize = 5;
$rootScope.rotate = false;
- element = $compile('')($rootScope);
+ element = $compile('
')($rootScope);
$rootScope.$digest();
});
@@ -381,9 +588,9 @@ describe('pagination directive', function () {
});
});
- describe('pagination directive with `boundary-links`', function () {
+ describe('pagination directive with `boundary-links`', function() {
beforeEach(function() {
- element = $compile('')($rootScope);
+ element = $compile('
')($rootScope);
$rootScope.$digest();
});
@@ -438,7 +645,7 @@ describe('pagination directive', function () {
});
it('changes "first" & "last" text from attributes', function() {
- element = $compile('')($rootScope);
+ element = $compile('
')($rootScope);
$rootScope.$digest();
expect(getPaginationEl(0).text()).toBe('<<<');
@@ -446,7 +653,7 @@ describe('pagination directive', function () {
});
it('changes "previous" & "next" text from attributes', function() {
- element = $compile('')($rootScope);
+ element = $compile('
')($rootScope);
$rootScope.$digest();
expect(getPaginationEl(1).text()).toBe('<<');
@@ -456,7 +663,7 @@ describe('pagination directive', function () {
it('changes "first" & "last" text from interpolated attributes', function() {
$rootScope.myfirstText = '<<<';
$rootScope.mylastText = '>>>';
- element = $compile('')($rootScope);
+ element = $compile('
')($rootScope);
$rootScope.$digest();
expect(getPaginationEl(0).text()).toBe('<<<');
@@ -466,43 +673,43 @@ describe('pagination directive', function () {
it('changes "previous" & "next" text from interpolated attributes', function() {
$rootScope.previousText = '<<';
$rootScope.nextText = '>>';
- element = $compile('')($rootScope);
+ element = $compile('
')($rootScope);
$rootScope.$digest();
expect(getPaginationEl(1).text()).toBe('<<');
expect(getPaginationEl(-2).text()).toBe('>>');
});
-
- it('should blur the "first" link after it has been clicked', function () {
- $document.find('body').append(element);
+
+ it('should blur the "first" link after it has been clicked', function() {
+ body.append(element);
var linkEl = getPaginationLinkEl(element, 0);
-
+
linkEl.focus();
expect(linkEl).toHaveFocus();
-
+
linkEl.click();
expect(linkEl).not.toHaveFocus();
-
+
element.remove();
});
-
- it('should blur the "last" link after it has been clicked', function () {
- $document.find('body').append(element);
+
+ it('should blur the "last" link after it has been clicked', function() {
+ body.append(element);
var linkEl = getPaginationLinkEl(element, -1);
-
+
linkEl.focus();
expect(linkEl).toHaveFocus();
-
+
linkEl.click();
expect(linkEl).not.toHaveFocus();
-
+
element.remove();
});
});
- describe('pagination directive with just number links', function () {
+ describe('pagination directive with just number links', function() {
beforeEach(function() {
- element = $compile('')($rootScope);
+ element = $compile('
')($rootScope);
$rootScope.$digest();
});
@@ -551,10 +758,10 @@ describe('pagination directive', function () {
});
});
- describe('with just boundary & number links', function () {
+ describe('with just boundary & number links', function() {
beforeEach(function() {
$rootScope.directions = false;
- element = $compile('')($rootScope);
+ element = $compile('
')($rootScope);
$rootScope.$digest();
});
@@ -583,10 +790,10 @@ describe('pagination directive', function () {
});
});
- describe('`num-pages`', function () {
+ describe('`num-pages`', function() {
beforeEach(function() {
$rootScope.numpg = null;
- element = $compile('')($rootScope);
+ element = $compile('
')($rootScope);
$rootScope.$digest();
expect(getPaginationEl(0).text()).toBe('FI');
@@ -640,24 +847,48 @@ describe('pagination directive', function () {
it('contains number of pages + 2 li elements', function() {
paginationConfig.itemsPerPage = 5;
- element = $compile('')($rootScope);
+ element = $compile('
')($rootScope);
$rootScope.$digest();
expect(getPaginationBarSize()).toBe(12);
});
- it('should take maxSize defaults into account', function () {
+ it('should take maxSize defaults into account', function() {
paginationConfig.maxSize = 2;
- element = $compile('')($rootScope);
+ element = $compile('
')($rootScope);
$rootScope.$digest();
expect(getPaginationBarSize()).toBe(4);
});
+
+ it('should take forceEllipses defaults into account', function () {
+ paginationConfig.forceEllipses = true;
+ element = $compile('
')($rootScope);
+ $rootScope.$digest();
+
+ // Should contain 2 nav buttons, 2 pages, and 2 ellipsis since the currentPage defaults to 3, which is in the middle
+ expect(getPaginationBarSize()).toBe(6);
+ });
+
+ it('should take boundaryLinkNumbers defaults into account', function () {
+ paginationConfig.boundaryLinkNumbers = true;
+ $rootScope.total = 88; // 9 pages
+ $rootScope.currentPage = 5;
+ element = $compile('
')($rootScope);
+ $rootScope.$digest();
+
+ // Should contain 2 nav buttons, 2 pages, 2 ellipsis, and 2 extra end numbers since the currentPage is in the middle
+ expect(getPaginationBarSize()).toBe(9);
+ expect(getPaginationAsText()).toBe('Previous, 1, ..., 4, 5, 6, ..., 9, Next');
+ });
});
- describe('override configuration from attributes', function () {
+ describe('override configuration from attributes', function() {
beforeEach(function() {
- element = $compile('')($rootScope);
+ $rootScope.pageLabel = function(id) {
+ return 'test_'+ id;
+ };
+ element = $compile('
')($rootScope);
$rootScope.$digest();
});
@@ -665,12 +896,68 @@ describe('pagination directive', function () {
expect(getPaginationBarSize()).toBe(9);
});
- it('should change paging text from attribute', function () {
+ it('should change paging text from attribute', function() {
expect(getPaginationEl(0).text()).toBe('<<');
expect(getPaginationEl(1).text()).toBe('<');
expect(getPaginationEl(-2).text()).toBe('>');
expect(getPaginationEl(-1).text()).toBe('>>');
});
+
+ it('has the label of the page as text in each page item', function() {
+ for (var i = 1; i <= 5; i++) {
+ // +1 because the first element is a <
+ expect(getPaginationEl(i+1).text()).toEqual('test_'+i);
+ }
+ });
+ });
+
+ describe('disabled with ngDisable', function() {
+ beforeEach(function() {
+ element = $compile('
')($rootScope);
+ $rootScope.currentPage = 3;
+ $rootScope.$digest();
+ });
+
+ it('should not respond to clicking', function() {
+ setDisabled(true);
+ clickPaginationEl(2);
+ expect($rootScope.currentPage).toBe(3);
+ setDisabled(false);
+ clickPaginationEl(2);
+ expect($rootScope.currentPage).toBe(2);
+ });
+
+ it('should change the class of all buttons except selected one', function() {
+ setDisabled(false);
+ expect(getPaginationEl(3).hasClass('active')).toBe(true);
+ expect(getPaginationEl(4).hasClass('active')).toBe(false);
+ setDisabled(true);
+ expect(getPaginationEl(3).hasClass('disabled')).toBe(false);
+ expect(getPaginationEl(4).hasClass('disabled')).toBe(true);
+ });
});
+});
+
+describe('pagination directive', function() {
+ var $compile, $rootScope, element;
+ beforeEach(module('ui.bootstrap.pagination'));
+ beforeEach(module('uib/template/pagination/pagination.html'));
+ beforeEach(inject(function(_$compile_, _$rootScope_) {
+ $compile = _$compile_;
+ $rootScope = _$rootScope_;
+ }));
+
+ it('should retain the model value when total-items starts as undefined', function() {
+ $rootScope.currentPage = 5;
+ $rootScope.total = undefined;
+ element = $compile('
content');
});
diff --git a/src/popover/docs/readme.md b/src/popover/docs/readme.md
index dcfd6dff6f..1e12a35b50 100644
--- a/src/popover/docs/readme.md
+++ b/src/popover/docs/readme.md
@@ -4,29 +4,122 @@ directive supports multiple placements, optional transition animation, and more.
Like the Bootstrap jQuery plugin, the popover **requires** the tooltip
module.
-There are two versions of the popover: `popover` and `popover-template`:
+__Note to mobile developers__: Please note that while popovers may work correctly on mobile devices (including tablets),
+ we have made the decision to not officially support such a use-case because it does not make sense from a UX perspective.
-- `popover` takes text only and will escape any HTML provided for the popover
- body.
-- `popover-template` takes text that specifies the location of a template to
- use for the popover body.
+There are three versions of the popover: `uib-popover` and `uib-popover-template`, and `uib-popover-html`:
-The popover directives provides several optional attributes to control how it
-will display:
+* `uib-popover` -
+ Takes text only and will escape any HTML provided for the popover body.
+* `uib-popover-html`
+ $ -
+ Takes an expression that evaluates to an HTML string. Note that this HTML is not compiled. If compilation is required, please use the `uib-popover-template` attribute option instead. *The user is responsible for ensuring the content is safe to put into the DOM!*
+* `uib-popover-template`
+ $ -
+ A URL representing the location of a template to use for the popover body. Note that the contents of this template need to be wrapped in a tag, e.g., ``.
-- `popover-title`: A string to display as a fancy title.
-- `popover-placement`: Where to place it? Defaults to "top", but also accepts
- "bottom", "left", "right".
-- `popover-animation`: Should it fade in and out? Defaults to "true".
-- `popover-popup-delay`: For how long should the user have to have the mouse
- over the element before the popover shows (in milliseconds)? Defaults to 0.
-- `popover-trigger`: What should trigger the show of the popover? See the
- `tooltip` directive for supported values.
-- `popover-append-to-body`: Should the tooltip be appended to `$body` instead of
- the parent element?
+### uib-popover-* settings
-The popover directives require the `$position` service.
+All these settings are available for the three types of popovers.
-The popover directive also supports various default configurations through the
-$tooltipProvider. See the [tooltip](#tooltip) section for more information.
+* `popover-animation`
+ $
+ C
+ _(Default: `true`, Config: `animation`)_ -
+ Should it fade in and out?
+* `popover-append-to-body`
+ $
+ C
+ _(Default: `false`, Config: `appendToBody`)_ -
+ Should the popover be appended to '$body' instead of the parent element?
+
+* `popover-class` -
+ Custom class to be applied to the popover.
+
+* `popover-enable`
+ $
+ _(Default: `true`)_ -
+ Is it enabled? It will enable or disable the configured popover-trigger.
+
+* `popover-is-open`
+
+ _(Default: `false`)_ -
+ Whether to show the popover.
+
+* `popover-placement`
+ C
+ _(Default: `top`, Config: `placement`)_ -
+ Passing in 'auto' separated by a space before the placement will enable auto positioning, e.g: "auto bottom-left". The popover will attempt to position where it fits in the closest scrollable ancestor. Accepts:
+
+ * `top` - popover on top, horizontally centered on host element.
+ * `top-left` - popover on top, left edge aligned with host element left edge.
+ * `top-right` - popover on top, right edge aligned with host element right edge.
+ * `bottom` - popover on bottom, horizontally centered on host element.
+ * `bottom-left` - popover on bottom, left edge aligned with host element left edge.
+ * `bottom-right` - popover on bottom, right edge aligned with host element right edge.
+ * `left` - popover on left, vertically centered on host element.
+ * `left-top` - popover on left, top edge aligned with host element top edge.
+ * `left-bottom` - popover on left, bottom edge aligned with host element bottom edge.
+ * `right` - popover on right, vertically centered on host element.
+ * `right-top` - popover on right, top edge aligned with host element top edge.
+ * `right-bottom` - popover on right, bottom edge aligned with host element bottom edge.
+
+* `popover-popup-close-delay`
+ C
+ _(Default: `0`, Config: `popupCloseDelay`)_ -
+ For how long should the popover remain open after the close trigger event?
+
+* `popover-popup-delay`
+ C
+ _(Default: `0`, Config: `popupDelay`)_ -
+ Popup delay in milliseconds until it opens.
+
+* `popover-title` -
+ A string to display as a fancy title.
+
+* `popover-trigger`
+ $
+ _(Default: `'click'`)_ -
+ What should trigger a show of the popover? Supports a space separated list of event names, or objects (see below).
+
+**Note:** To configure the tooltips, you need to do it on `$uibTooltipProvider` (also see below).
+
+### Triggers
+
+The following show triggers are supported out of the box, along with their provided hide triggers:
+
+- `mouseenter`: `mouseleave`
+- `click`: `click`
+- `outsideClick`: `outsideClick`
+- `focus`: `blur`
+- `none`
+
+The `outsideClick` trigger will cause the popover to toggle on click, and hide when anything else is clicked.
+
+For any non-supported value, the trigger will be used to both show and hide the
+popover. Using the 'none' trigger will disable the internal trigger(s), one can
+then use the `popover-is-open` attribute exclusively to show and hide the popover.
+
+### $uibTooltipProvider
+
+Through the `$uibTooltipProvider`, you can change the way tooltips and popovers
+behave by default; the attributes above always take precedence. The following
+methods are available:
+
+* `setTriggers(obj)`
+ _(Example: `{ 'openTrigger': 'closeTrigger' }`)_ -
+ Extends the default trigger mappings mentioned above with mappings of your own.
+
+* `options(obj)` -
+ Provide a set of defaults for certain tooltip and popover attributes. Currently supports the ones with the C badge.
+
+### Known issues
+
+For Safari 7+ support, if you want to use **focus** `popover-trigger`, you need to use an anchor tag with a tab index. For example:
+
+```
+
+ Click Me
+
+```
diff --git a/src/popover/index-nocss.js b/src/popover/index-nocss.js
new file mode 100644
index 0000000000..9d90984095
--- /dev/null
+++ b/src/popover/index-nocss.js
@@ -0,0 +1,11 @@
+require('../tooltip/index-nocss.js');
+require('../../template/popover/popover.html.js');
+require('../../template/popover/popover-html.html.js');
+require('../../template/popover/popover-template.html.js');
+require('./popover');
+
+var MODULE_NAME = 'ui.bootstrap.module.popover';
+
+angular.module(MODULE_NAME, ['ui.bootstrap.popover', 'uib/template/popover/popover.html', 'uib/template/popover/popover-html.html', 'uib/template/popover/popover-template.html']);
+
+module.exports = MODULE_NAME;
diff --git a/src/popover/index.js b/src/popover/index.js
new file mode 100644
index 0000000000..03c464a53a
--- /dev/null
+++ b/src/popover/index.js
@@ -0,0 +1,2 @@
+require('../tooltip/tooltip.css');
+module.exports = require('./index-nocss.js');
diff --git a/src/popover/popover.js b/src/popover/popover.js
index dadf2dabc9..df94a4c96f 100644
--- a/src/popover/popover.js
+++ b/src/popover/popover.js
@@ -1,35 +1,46 @@
/**
* The following features are still outstanding: popup delay, animation as a
* function, placement as a function, inside, support for more triggers than
- * just mouse enter/leave, html popovers, and selector delegatation.
+ * just mouse enter/leave, and selector delegatation.
*/
-angular.module( 'ui.bootstrap.popover', [ 'ui.bootstrap.tooltip' ] )
+angular.module('ui.bootstrap.popover', ['ui.bootstrap.tooltip'])
-.directive( 'popoverTemplatePopup', function () {
+.directive('uibPopoverTemplatePopup', function() {
return {
- restrict: 'EA',
- replace: true,
- scope: { title: '@', contentExp: '&', placement: '@', popupClass: '@', animation: '&', isOpen: '&',
- originScope: '&' },
- templateUrl: 'template/popover/popover-template.html'
+ restrict: 'A',
+ scope: { uibTitle: '@', contentExp: '&', originScope: '&' },
+ templateUrl: 'uib/template/popover/popover-template.html'
};
})
-.directive( 'popoverTemplate', [ '$tooltip', function ( $tooltip ) {
- return $tooltip( 'popoverTemplate', 'popover', 'click', {
+.directive('uibPopoverTemplate', ['$uibTooltip', function($uibTooltip) {
+ return $uibTooltip('uibPopoverTemplate', 'popover', 'click', {
useContentExp: true
- } );
+ });
}])
-.directive( 'popoverPopup', function () {
+.directive('uibPopoverHtmlPopup', function() {
return {
- restrict: 'EA',
- replace: true,
- scope: { title: '@', content: '@', placement: '@', popupClass: '@', animation: '&', isOpen: '&' },
- templateUrl: 'template/popover/popover.html'
+ restrict: 'A',
+ scope: { contentExp: '&', uibTitle: '@' },
+ templateUrl: 'uib/template/popover/popover-html.html'
};
})
-.directive( 'popover', [ '$tooltip', function ( $tooltip ) {
- return $tooltip( 'popover', 'popover', 'click' );
+.directive('uibPopoverHtml', ['$uibTooltip', function($uibTooltip) {
+ return $uibTooltip('uibPopoverHtml', 'popover', 'click', {
+ useContentExp: true
+ });
+}])
+
+.directive('uibPopoverPopup', function() {
+ return {
+ restrict: 'A',
+ scope: { uibTitle: '@', content: '@' },
+ templateUrl: 'uib/template/popover/popover.html'
+ };
+})
+
+.directive('uibPopover', ['$uibTooltip', function($uibTooltip) {
+ return $uibTooltip('uibPopover', 'popover', 'click');
}]);
diff --git a/src/popover/test/popover-html.spec.js b/src/popover/test/popover-html.spec.js
new file mode 100644
index 0000000000..fd43e51f15
--- /dev/null
+++ b/src/popover/test/popover-html.spec.js
@@ -0,0 +1,212 @@
+describe('popover', function() {
+ var elm,
+ elmBody,
+ scope,
+ elmScope,
+ tooltipScope;
+
+ // load the popover code
+ beforeEach(module('ui.bootstrap.popover'));
+
+ // load the template
+ beforeEach(module('uib/template/popover/popover-html.html'));
+
+ beforeEach(inject(function($rootScope, $compile, $sce, _$document_) {
+ $document = _$document_;
+ elmBody = angular.element(
+ '
Selector Text
'
+ );
+
+ scope = $rootScope;
+ scope.template = $sce.trustAsHtml('My template');
+ $compile(elmBody)(scope);
+ scope.$digest();
+ $document.find('body').append(elmBody);
+ elm = elmBody.find('span');
+ elmScope = elm.scope();
+ tooltipScope = elmScope.$$childTail;
+ }));
+
+ afterEach(function() {
+ $document.off('keypress');
+ });
+
+ it('should not be open initially', inject(function() {
+ expect(tooltipScope.isOpen).toBe(false);
+
+ // We can only test *that* the popover-popup element wasn't created as the
+ // implementation is templated and replaced.
+ expect(elmBody.children().length).toBe(1);
+ }));
+
+ it('should open on click', inject(function() {
+ elm.trigger('click');
+ tooltipScope.$digest();
+ expect(tooltipScope.isOpen).toBe(true);
+
+ // We can only test *that* the popover-popup element was created as the
+ // implementation is templated and replaced.
+ expect(elmBody.children().length).toBe(2);
+ }));
+
+ it('should close on second click', inject(function() {
+ elm.trigger('click');
+ tooltipScope.$digest();
+ expect(tooltipScope.isOpen).toBe(true);
+ elm.trigger('click');
+ tooltipScope.$digest();
+ expect(tooltipScope.isOpen).toBe(false);
+ }));
+
+ it('should not open on click if template is empty', inject(function() {
+ scope.template = null;
+ scope.$digest();
+
+ elm.trigger('click');
+ tooltipScope.$digest();
+ expect(tooltipScope.isOpen).toBe(false);
+
+ expect(elmBody.children().length).toBe(1);
+ }));
+
+ it('should show updated text', inject(function($sce) {
+ scope.template = $sce.trustAsHtml('My template');
+ scope.$digest();
+
+ elm.trigger('click');
+ tooltipScope.$digest();
+ expect(tooltipScope.isOpen).toBe(true);
+
+ expect(elmBody.children().eq(1).text().trim()).toBe('My template');
+
+ scope.template = $sce.trustAsHtml('Another template');
+ scope.$digest();
+
+ expect(elmBody.children().eq(1).text().trim()).toBe('Another template');
+ }));
+
+ it('should hide popover when template becomes empty', inject(function($timeout) {
+ elm.trigger('click');
+ tooltipScope.$digest();
+ $timeout.flush(0);
+ expect(tooltipScope.isOpen).toBe(true);
+
+ scope.template = '';
+ scope.$digest();
+
+ expect(tooltipScope.isOpen).toBe(false);
+
+ $timeout.flush();
+ expect(elmBody.children().length).toBe(1);
+ }));
+
+
+ it('should not unbind event handlers created by other directives - issue 456', inject(function($compile) {
+ scope.click = function() {
+ scope.clicked = !scope.clicked;
+ };
+
+ elmBody = angular.element(
+ ''
+ );
+ $compile(elmBody)(scope);
+ scope.$digest();
+
+ elm = elmBody.find('input');
+
+ elm.trigger('mouseenter');
+ tooltipScope.$digest();
+ elm.trigger('mouseleave');
+ tooltipScope.$digest();
+ expect(scope.clicked).toBeFalsy();
+
+ elm.click();
+ expect(scope.clicked).toBeTruthy();
+ }));
+
+ it('should popup with animate class by default', inject(function() {
+ elm.trigger('click');
+ tooltipScope.$digest();
+ expect(tooltipScope.isOpen).toBe(true);
+
+ expect(elmBody.children().eq(1)).toHaveClass('fade');
+ }));
+
+ it('should popup without animate class when animation disabled', inject(function($compile) {
+ elmBody = angular.element(
+ '
\ No newline at end of file
diff --git a/src/position/docs/demo.js b/src/position/docs/demo.js
new file mode 100644
index 0000000000..8be1343de4
--- /dev/null
+++ b/src/position/docs/demo.js
@@ -0,0 +1,27 @@
+angular.module('ui.bootstrap.demo').controller('PositionDemoCtrl', function ($scope, $window, $uibPosition) {
+
+ $scope.elemVals = {};
+ $scope.parentScrollable = true;
+ $scope.parentRelative = true;
+
+ $scope.getValues = function() {
+ var divEl = $window.document.querySelector('#posdemodiv');
+ var btnEl = $window.document.querySelector('#posdemobtn');
+
+ var offsetParent = $uibPosition.offsetParent(divEl);
+ $scope.elemVals.offsetParent = 'type: ' + offsetParent.tagName + ', id: ' + offsetParent.id;
+
+ var scrollParent = $uibPosition.scrollParent(divEl);
+ $scope.elemVals.scrollParent = 'type: ' + scrollParent.tagName + ', id: ' + scrollParent.id;
+
+ $scope.scrollbarWidth = $uibPosition.scrollbarWidth();
+
+ $scope.elemVals.position = $uibPosition.position(divEl);
+
+ $scope.elemVals.offset = $uibPosition.offset(divEl);
+
+ $scope.elemVals.viewportOffset = $uibPosition.viewportOffset(divEl);
+
+ $scope.elemVals.positionElements = $uibPosition.positionElements(btnEl, divEl, 'auto bottom-left');
+ };
+});
\ No newline at end of file
diff --git a/src/position/docs/readme.md b/src/position/docs/readme.md
new file mode 100644
index 0000000000..1dad79aca0
--- /dev/null
+++ b/src/position/docs/readme.md
@@ -0,0 +1,336 @@
+The `$uibPosition` service provides a set of DOM utilities used internally to absolute-position an element in relation to another element (tooltips, popovers, typeaheads etc...).
+
+#### getRawNode(element)
+
+Takes a jQuery/jqLite element and converts it to a raw DOM element.
+
+##### parameters
+
+* `element`
+ _(Type: `object`)_ -
+ The element to convert.
+
+##### returns
+
+* _(Type: `element`)_ -
+ A raw DOM element.
+
+#### parseStyle(element)
+
+Parses a numeric style value to a number. Strips units and will return 0 for invalid (NaN) numbers.
+
+##### parameters
+
+* `value`
+ _(Type: `string`)_ -
+ The style value to parse.
+
+##### returns
+
+* _(Type: `number`)_ -
+ The numeric value of the style property.
+
+#### offsetParent(element)
+
+Gets the closest positioned ancestor.
+
+##### parameters
+
+* `element`
+ _(Type: `element`)_ -
+ The element to get the offset parent for.
+
+##### returns
+
+* _(Type: `element`)_ -
+ The closest positioned ancestor.
+
+#### scrollbarWidth(isBody)
+
+Calculates the browser scrollbar width and caches the result for future calls. Concept from the TWBS measureScrollbar() function in [modal.js](https://github.com/twbs/bootstrap/blob/master/js/modal.js).
+
+##### parameters
+
+* `isBody`
+ _(Type: `boolean`, Default: `false`, optional)_ - Is the requested scrollbar width for the body/html element. IE and Edge overlay the scrollbar on the body/html element and should be considered 0.
+
+##### returns
+
+* _(Type: `number`)_ -
+ The width of the browser scrollbar.
+
+#### scrollbarPadding(element)
+
+Calculates the padding required to replace the scrollbar on an element.
+
+##### parameters
+
+* 'element' _(Type: `element`)_ - The element to calculate the padding on (should be a scrollable element).
+
+##### returns
+
+An object with the following properties:
+
+* `scrollbarWidth`
+ _(Type: `number`)_ -
+ The width of the scrollbar.
+
+* `widthOverflow`
+ _(Type: `boolean`)_ -
+ Whether the width is overflowing.
+
+* `right`
+ _(Type: `number`)_ -
+ The total right padding required to replace the scrollbar.
+
+* `originalRight`
+ _(Type: `number`)_ -
+ The oringal right padding on the element.
+
+* `heightOverflow`
+ _(Type: `boolean`)_ -
+ Whether the height is overflowing.
+
+* `bottom`
+ _(Type: `number`)_ -
+ The total bottom padding required to replace the scrollbar.
+
+* `originalBottom`
+ _(Type: `number`)_ -
+ The oringal bottom padding on the element.
+
+#### isScrollable(element, includeHidden)
+
+Determines if an element is scrollable.
+
+##### parameters
+
+* `element`
+ _(Type: `element`)_ -
+ The element to check.
+
+* `includeHidden`
+ _(Type: `boolean`, Default: `false`, optional)_ - Should scroll style of 'hidden' be considered.
+
+##### returns
+
+* _(Type: `boolean`)_ -
+ Whether the element is scrollable.
+
+#### scrollParent(element, includeHidden, includeSelf)
+
+Gets the closest scrollable ancestor. Concept from the jQueryUI [scrollParent.js](https://github.com/jquery/jquery-ui/blob/master/ui/scroll-parent.js).
+
+##### parameters
+
+* `element`
+ _(Type: `element`)_ -
+ The element to get the closest scrollable ancestor for.
+
+* `includeHidden`
+ _(Type: `boolean`, Default: `false`, optional)_ - Should scroll style of 'hidden' be considered.
+
+* `includeSelf`
+ _(Type: `boolean`, Default: `false`, optional)_ - Should the element passed in be included in the scrollable lookup.
+
+##### returns
+
+* _(Type: `element`)_ -
+ The closest scrollable ancestor.
+
+#### position(element, includeMargins)
+
+A read-only equivalent of jQuery's [position](http://api.jquery.com/position/) function, distance to closest positioned ancestor. Does not account for margins by default like jQuery's position.
+
+##### parameters
+
+* `element` _(Type: `element`)_ -
+ The element to get the position for.
+
+* `includeMargins` _(Type: `boolean`, Default: `false`, optional)_ -
+ Should margins be accounted for.
+
+##### returns
+
+An object with the following properties:
+
+* `width`
+ _(Type: `number`)_ -
+ The width of the element.
+
+* `height`
+ _(Type: `number`)_ -
+ The height of the element.
+
+* `top`
+ _(Type: `number`)_ -
+ Distance to top edge of offset parent.
+
+* `left`
+ _(Type: `number`)_ -
+ Distance to left edge of offset parent.
+
+#### offset(element)
+
+A read-only equivalent of jQuery's [offset](http://api.jquery.com/offset/) function, distance to viewport.
+
+##### parameters
+
+* `element`
+ _(Type: `element`)_ -
+ The element to get the offset for.
+
+##### returns
+
+An object with the following properties:
+
+* `width`
+ _(Type: `number`)_ -
+ The width of the element.
+
+* `height`
+ _(Type: `number`)_ -
+ The height of the element.
+
+* `top`
+ _(Type: `number`)_ -
+ Distance to top edge of the viewport.
+
+* `left`
+ _(Type: `number`)_ -
+ Distance to left edge of the viewport.
+
+#### viewportOffset(element, useDocument, includePadding)
+
+Gets the elements available space relative to the closest scrollable ancestor. Accounts for padding, border, and scrollbar width.
+Right and bottom dimensions represent the distance to the respective edge of the viewport element, not the top and left edge.
+If the element edge extends beyond the viewport, a negative value will be reported.
+
+##### parameters
+
+* `element`
+ _(Type: `element`)_ -
+ The element to get the viewport offset for.
+
+* `useDocument`
+ _(Type: `boolean`, Default: `false`, optional)_ -
+ Should the viewport be the document element instead of the first scrollable element.
+
+* `includePadding`
+ _(Type: `boolean`, Default: `true`, optional)_ -
+ Should the padding on the viewport element be accounted for, default is true.
+
+##### returns
+
+An object with the following properties:
+
+* `top`
+ _(Type: `number`)_ -
+ Distance to top content edge of the viewport.
+
+* `bottom`
+ _(Type: `number`)_ -
+ Distance to bottom content edge of the viewport.
+
+* `left`
+ _(Type: `number`)_ -
+ Distance to left content edge of the viewport.
+
+* `right`
+ _(Type: `number`)_ -
+ Distance to right content edge of the viewport.
+
+#### parsePlacement(placement)
+
+Gets an array of placement values parsed from a placement string. Along with the 'auto' indicator, supported placement strings are:
+
+* top: element on top, horizontally centered on host element.
+* top-left: element on top, left edge aligned with host element left edge.
+* top-right: element on top, right edge aligned with host element right edge.
+* bottom: element on bottom, horizontally centered on host element.
+* bottom-left: element on bottom, left edge aligned with host element left edge.
+* bottom-right: element on bottom, right edge aligned with host element right edge.
+* left: element on left, vertically centered on host element.
+* left-top: element on left, top edge aligned with host element top edge.
+* left-bottom: element on left, bottom edge aligned with host element bottom edge.
+* right: element on right, vertically centered on host element.
+* right-top: element on right, top edge aligned with host element top edge.
+* right-bottom: element on right, bottom edge aligned with host element bottom edge.
+
+A placement string with an 'auto' indicator is expected to be space separated from the placement, i.e: 'auto bottom-left'.
+If the primary and secondary placement values do not match 'top, bottom, left, right' then 'top' will be the primary placement and
+'center' will be the secondary placement. If 'auto' is passed, true will be returned as the 3rd value of the array.
+
+##### parameters
+
+* `placement`
+ _(Type: `string`, Example: `auto top-left`)_ -
+ The placement string to parse.
+
+##### returns
+
+An array with the following values:
+
+* `[0]`
+ _(Type: `string`)_ -
+ The primary placement.
+
+* `[1]`
+ _(Type: `string`)_ -
+ The secondary placement.
+
+* `[2]`
+ _(Type: `boolean`)_ -
+ Is auto place enabled.
+
+#### positionElements(hostElement, targetElement, placement, appendToBody)
+
+Gets gets coordinates for an element to be positioned relative to another element.
+
+##### parameters
+
+* `hostElement`
+ _(Type: `element`)_ -
+ The element to position against.
+
+* `targetElement`
+ _(Type: `element`)_ -
+ The element to position.
+
+* `placement`
+ _(Type: `string`, Default: `top`, optional)_ -
+ The placement for the target element. See the parsePlacement() function for available options. If 'auto' placement is used, the viewportOffset() function is used to decide where the targetElement will fit.
+
+* `appendToBody`
+ _(Type: `boolean`, Default: `false`, optional)_ -
+ Should the coordinates be cacluated from the body element.
+
+##### returns
+
+An object with the following properties:
+
+* `top`
+ _(Type: `number`)_ -
+ The targetElement top value.
+
+* `left`
+ _(Type: `number`)_ -
+ The targetElement left value.
+
+* `right`
+ _(Type: `number`)_ -
+ The resolved placement with 'auto' removed.
+
+#### positionArrow(element, placement)
+
+Positions the tooltip and popover arrow elements when using placement options beyond the standard top, left, bottom, or right.
+
+##### parameters
+
+* `element`
+ _(Type: `element`)_ -
+ The element to position the arrow element for.
+
+* `placement`
+ _(Type: `string`)_ -
+ The placement for the element.
diff --git a/src/position/index-nocss.js b/src/position/index-nocss.js
new file mode 100644
index 0000000000..10b8e9e506
--- /dev/null
+++ b/src/position/index-nocss.js
@@ -0,0 +1,7 @@
+require('./position');
+
+var MODULE_NAME = 'ui.bootstrap.module.position';
+
+angular.module(MODULE_NAME, ['ui.bootstrap.position']);
+
+module.exports = MODULE_NAME;
diff --git a/src/position/index.js b/src/position/index.js
new file mode 100644
index 0000000000..400b560ca1
--- /dev/null
+++ b/src/position/index.js
@@ -0,0 +1,2 @@
+require('./position.css');
+module.exports = require('./index-nocss.js');
diff --git a/src/position/position.css b/src/position/position.css
new file mode 100644
index 0000000000..41fb4a2d87
--- /dev/null
+++ b/src/position/position.css
@@ -0,0 +1,19 @@
+.uib-position-measure {
+ display: block !important;
+ visibility: hidden !important;
+ position: absolute !important;
+ top: -9999px !important;
+ left: -9999px !important;
+}
+
+.uib-position-scrollbar-measure {
+ position: absolute !important;
+ top: -9999px !important;
+ width: 50px !important;
+ height: 50px !important;
+ overflow: scroll !important;
+}
+
+.uib-position-body-scrollbar-measure {
+ overflow: scroll !important;
+}
\ No newline at end of file
diff --git a/src/position/position.js b/src/position/position.js
index 3444c33449..3ab066abb5 100644
--- a/src/position/position.js
+++ b/src/position/position.js
@@ -1,152 +1,620 @@
angular.module('ui.bootstrap.position', [])
/**
- * A set of utility methods that can be use to retrieve position of DOM elements.
- * It is meant to be used where we need to absolute-position DOM elements in
- * relation to other, existing elements (this is the case for tooltips, popovers,
+ * A set of utility methods for working with the DOM.
+ * It is meant to be used where we need to absolute-position elements in
+ * relation to another element (this is the case for tooltips, popovers,
* typeahead suggestions etc.).
*/
- .factory('$position', ['$document', '$window', function ($document, $window) {
-
- function getStyle(el, cssprop) {
- if (el.currentStyle) { //IE
- return el.currentStyle[cssprop];
- } else if ($window.getComputedStyle) {
- return $window.getComputedStyle(el)[cssprop];
- }
- // finally try and get inline style
- return el.style[cssprop];
- }
-
+ .factory('$uibPosition', ['$document', '$window', function($document, $window) {
/**
- * Checks if a given element is statically positioned
- * @param element - raw DOM element
+ * Used by scrollbarWidth() function to cache scrollbar's width.
+ * Do not access this variable directly, use scrollbarWidth() instead.
*/
- function isStaticPositioned(element) {
- return (getStyle(element, 'position') || 'static' ) === 'static';
- }
-
+ var SCROLLBAR_WIDTH;
/**
- * returns the closest, non-statically positioned parentOffset of a given element
- * @param element
+ * scrollbar on body and html element in IE and Edge overlay
+ * content and should be considered 0 width.
*/
- var parentOffsetEl = function (element) {
- var docDomEl = $document[0];
- var offsetParent = element.offsetParent || docDomEl;
- while (offsetParent && offsetParent !== docDomEl && isStaticPositioned(offsetParent) ) {
- offsetParent = offsetParent.offsetParent;
- }
- return offsetParent || docDomEl;
+ var BODY_SCROLLBAR_WIDTH;
+ var OVERFLOW_REGEX = {
+ normal: /(auto|scroll)/,
+ hidden: /(auto|scroll|hidden)/
};
+ var PLACEMENT_REGEX = {
+ auto: /\s?auto?\s?/i,
+ primary: /^(top|bottom|left|right)$/,
+ secondary: /^(top|bottom|left|right|center)$/,
+ vertical: /^(top|bottom)$/
+ };
+ var BODY_REGEX = /(HTML|BODY)/;
return {
+
+ /**
+ * Provides a raw DOM element from a jQuery/jQLite element.
+ *
+ * @param {element} elem - The element to convert.
+ *
+ * @returns {element} A HTML element.
+ */
+ getRawNode: function(elem) {
+ return elem.nodeName ? elem : elem[0] || elem;
+ },
+
+ /**
+ * Provides a parsed number for a style property. Strips
+ * units and casts invalid numbers to 0.
+ *
+ * @param {string} value - The style value to parse.
+ *
+ * @returns {number} A valid number.
+ */
+ parseStyle: function(value) {
+ value = parseFloat(value);
+ return isFinite(value) ? value : 0;
+ },
+
+ /**
+ * Provides the closest positioned ancestor.
+ *
+ * @param {element} element - The element to get the offest parent for.
+ *
+ * @returns {element} The closest positioned ancestor.
+ */
+ offsetParent: function(elem) {
+ elem = this.getRawNode(elem);
+
+ var offsetParent = elem.offsetParent || $document[0].documentElement;
+
+ function isStaticPositioned(el) {
+ return ($window.getComputedStyle(el).position || 'static') === 'static';
+ }
+
+ while (offsetParent && offsetParent !== $document[0].documentElement && isStaticPositioned(offsetParent)) {
+ offsetParent = offsetParent.offsetParent;
+ }
+
+ return offsetParent || $document[0].documentElement;
+ },
+
+ /**
+ * Provides the scrollbar width, concept from TWBS measureScrollbar()
+ * function in https://github.com/twbs/bootstrap/blob/master/js/modal.js
+ * In IE and Edge, scollbar on body and html element overlay and should
+ * return a width of 0.
+ *
+ * @returns {number} The width of the browser scollbar.
+ */
+ scrollbarWidth: function(isBody) {
+ if (isBody) {
+ if (angular.isUndefined(BODY_SCROLLBAR_WIDTH)) {
+ var bodyElem = $document.find('body');
+ bodyElem.addClass('uib-position-body-scrollbar-measure');
+ BODY_SCROLLBAR_WIDTH = $window.innerWidth - bodyElem[0].clientWidth;
+ BODY_SCROLLBAR_WIDTH = isFinite(BODY_SCROLLBAR_WIDTH) ? BODY_SCROLLBAR_WIDTH : 0;
+ bodyElem.removeClass('uib-position-body-scrollbar-measure');
+ }
+ return BODY_SCROLLBAR_WIDTH;
+ }
+
+ if (angular.isUndefined(SCROLLBAR_WIDTH)) {
+ var scrollElem = angular.element('');
+ $document.find('body').append(scrollElem);
+ SCROLLBAR_WIDTH = scrollElem[0].offsetWidth - scrollElem[0].clientWidth;
+ SCROLLBAR_WIDTH = isFinite(SCROLLBAR_WIDTH) ? SCROLLBAR_WIDTH : 0;
+ scrollElem.remove();
+ }
+
+ return SCROLLBAR_WIDTH;
+ },
+
+ /**
+ * Provides the padding required on an element to replace the scrollbar.
+ *
+ * @returns {object} An object with the following properties:
+ *
+ *
**scrollbarWidth**: the width of the scrollbar
+ *
**widthOverflow**: whether the the width is overflowing
+ *
**right**: the amount of right padding on the element needed to replace the scrollbar
+ *
**rightOriginal**: the amount of right padding currently on the element
+ *
**heightOverflow**: whether the the height is overflowing
+ *
**bottom**: the amount of bottom padding on the element needed to replace the scrollbar
+ *
**bottomOriginal**: the amount of bottom padding currently on the element
+ *
+ */
+ scrollbarPadding: function(elem) {
+ elem = this.getRawNode(elem);
+
+ var elemStyle = $window.getComputedStyle(elem);
+ var paddingRight = this.parseStyle(elemStyle.paddingRight);
+ var paddingBottom = this.parseStyle(elemStyle.paddingBottom);
+ var scrollParent = this.scrollParent(elem, false, true);
+ var scrollbarWidth = this.scrollbarWidth(BODY_REGEX.test(scrollParent.tagName));
+
+ return {
+ scrollbarWidth: scrollbarWidth,
+ widthOverflow: scrollParent.scrollWidth > scrollParent.clientWidth,
+ right: paddingRight + scrollbarWidth,
+ originalRight: paddingRight,
+ heightOverflow: scrollParent.scrollHeight > scrollParent.clientHeight,
+ bottom: paddingBottom + scrollbarWidth,
+ originalBottom: paddingBottom
+ };
+ },
+
+ /**
+ * Checks to see if the element is scrollable.
+ *
+ * @param {element} elem - The element to check.
+ * @param {boolean=} [includeHidden=false] - Should scroll style of 'hidden' be considered,
+ * default is false.
+ *
+ * @returns {boolean} Whether the element is scrollable.
+ */
+ isScrollable: function(elem, includeHidden) {
+ elem = this.getRawNode(elem);
+
+ var overflowRegex = includeHidden ? OVERFLOW_REGEX.hidden : OVERFLOW_REGEX.normal;
+ var elemStyle = $window.getComputedStyle(elem);
+ return overflowRegex.test(elemStyle.overflow + elemStyle.overflowY + elemStyle.overflowX);
+ },
+
+ /**
+ * Provides the closest scrollable ancestor.
+ * A port of the jQuery UI scrollParent method:
+ * https://github.com/jquery/jquery-ui/blob/master/ui/scroll-parent.js
+ *
+ * @param {element} elem - The element to find the scroll parent of.
+ * @param {boolean=} [includeHidden=false] - Should scroll style of 'hidden' be considered,
+ * default is false.
+ * @param {boolean=} [includeSelf=false] - Should the element being passed be
+ * included in the scrollable llokup.
+ *
+ * @returns {element} A HTML element.
+ */
+ scrollParent: function(elem, includeHidden, includeSelf) {
+ elem = this.getRawNode(elem);
+
+ var overflowRegex = includeHidden ? OVERFLOW_REGEX.hidden : OVERFLOW_REGEX.normal;
+ var documentEl = $document[0].documentElement;
+ var elemStyle = $window.getComputedStyle(elem);
+ if (includeSelf && overflowRegex.test(elemStyle.overflow + elemStyle.overflowY + elemStyle.overflowX)) {
+ return elem;
+ }
+ var excludeStatic = elemStyle.position === 'absolute';
+ var scrollParent = elem.parentElement || documentEl;
+
+ if (scrollParent === documentEl || elemStyle.position === 'fixed') {
+ return documentEl;
+ }
+
+ while (scrollParent.parentElement && scrollParent !== documentEl) {
+ var spStyle = $window.getComputedStyle(scrollParent);
+ if (excludeStatic && spStyle.position !== 'static') {
+ excludeStatic = false;
+ }
+
+ if (!excludeStatic && overflowRegex.test(spStyle.overflow + spStyle.overflowY + spStyle.overflowX)) {
+ break;
+ }
+ scrollParent = scrollParent.parentElement;
+ }
+
+ return scrollParent;
+ },
+
/**
* Provides read-only equivalent of jQuery's position function:
- * http://api.jquery.com/position/
+ * http://api.jquery.com/position/ - distance to closest positioned
+ * ancestor. Does not account for margins by default like jQuery position.
+ *
+ * @param {element} elem - The element to caclulate the position on.
+ * @param {boolean=} [includeMargins=false] - Should margins be accounted
+ * for, default is false.
+ *
+ * @returns {object} An object with the following properties:
+ *
+ *
**width**: the width of the element
+ *
**height**: the height of the element
+ *
**top**: distance to top edge of offset parent
+ *
**left**: distance to left edge of offset parent
+ *
*/
- position: function (element) {
- var elBCR = this.offset(element);
- var offsetParentBCR = { top: 0, left: 0 };
- var offsetParentEl = parentOffsetEl(element[0]);
- if (offsetParentEl != $document[0]) {
- offsetParentBCR = this.offset(angular.element(offsetParentEl));
- offsetParentBCR.top += offsetParentEl.clientTop - offsetParentEl.scrollTop;
- offsetParentBCR.left += offsetParentEl.clientLeft - offsetParentEl.scrollLeft;
+ position: function(elem, includeMagins) {
+ elem = this.getRawNode(elem);
+
+ var elemOffset = this.offset(elem);
+ if (includeMagins) {
+ var elemStyle = $window.getComputedStyle(elem);
+ elemOffset.top -= this.parseStyle(elemStyle.marginTop);
+ elemOffset.left -= this.parseStyle(elemStyle.marginLeft);
+ }
+ var parent = this.offsetParent(elem);
+ var parentOffset = {top: 0, left: 0};
+
+ if (parent !== $document[0].documentElement) {
+ parentOffset = this.offset(parent);
+ parentOffset.top += parent.clientTop - parent.scrollTop;
+ parentOffset.left += parent.clientLeft - parent.scrollLeft;
}
- var boundingClientRect = element[0].getBoundingClientRect();
return {
- width: boundingClientRect.width || element.prop('offsetWidth'),
- height: boundingClientRect.height || element.prop('offsetHeight'),
- top: elBCR.top - offsetParentBCR.top,
- left: elBCR.left - offsetParentBCR.left
+ width: Math.round(angular.isNumber(elemOffset.width) ? elemOffset.width : elem.offsetWidth),
+ height: Math.round(angular.isNumber(elemOffset.height) ? elemOffset.height : elem.offsetHeight),
+ top: Math.round(elemOffset.top - parentOffset.top),
+ left: Math.round(elemOffset.left - parentOffset.left)
};
},
/**
* Provides read-only equivalent of jQuery's offset function:
- * http://api.jquery.com/offset/
+ * http://api.jquery.com/offset/ - distance to viewport. Does
+ * not account for borders, margins, or padding on the body
+ * element.
+ *
+ * @param {element} elem - The element to calculate the offset on.
+ *
+ * @returns {object} An object with the following properties:
+ *
+ *
**width**: the width of the element
+ *
**height**: the height of the element
+ *
**top**: distance to top edge of viewport
+ *
**right**: distance to bottom edge of viewport
+ *
*/
- offset: function (element) {
- var boundingClientRect = element[0].getBoundingClientRect();
+ offset: function(elem) {
+ elem = this.getRawNode(elem);
+
+ var elemBCR = elem.getBoundingClientRect();
return {
- width: boundingClientRect.width || element.prop('offsetWidth'),
- height: boundingClientRect.height || element.prop('offsetHeight'),
- top: boundingClientRect.top + ($window.pageYOffset || $document[0].documentElement.scrollTop),
- left: boundingClientRect.left + ($window.pageXOffset || $document[0].documentElement.scrollLeft)
+ width: Math.round(angular.isNumber(elemBCR.width) ? elemBCR.width : elem.offsetWidth),
+ height: Math.round(angular.isNumber(elemBCR.height) ? elemBCR.height : elem.offsetHeight),
+ top: Math.round(elemBCR.top + ($window.pageYOffset || $document[0].documentElement.scrollTop)),
+ left: Math.round(elemBCR.left + ($window.pageXOffset || $document[0].documentElement.scrollLeft))
};
},
/**
- * Provides coordinates for the targetEl in relation to hostEl
+ * Provides offset distance to the closest scrollable ancestor
+ * or viewport. Accounts for border and scrollbar width.
+ *
+ * Right and bottom dimensions represent the distance to the
+ * respective edge of the viewport element. If the element
+ * edge extends beyond the viewport, a negative value will be
+ * reported.
+ *
+ * @param {element} elem - The element to get the viewport offset for.
+ * @param {boolean=} [useDocument=false] - Should the viewport be the document element instead
+ * of the first scrollable element, default is false.
+ * @param {boolean=} [includePadding=true] - Should the padding on the offset parent element
+ * be accounted for, default is true.
+ *
+ * @returns {object} An object with the following properties:
+ *
+ *
**top**: distance to the top content edge of viewport element
+ *
**bottom**: distance to the bottom content edge of viewport element
+ *
**left**: distance to the left content edge of viewport element
+ *
**right**: distance to the right content edge of viewport element
top: element on top, horizontally centered on host element.
+ *
top-left: element on top, left edge aligned with host element left edge.
+ *
top-right: element on top, lerightft edge aligned with host element right edge.
+ *
bottom: element on bottom, horizontally centered on host element.
+ *
bottom-left: element on bottom, left edge aligned with host element left edge.
+ *
bottom-right: element on bottom, right edge aligned with host element right edge.
+ *
left: element on left, vertically centered on host element.
+ *
left-top: element on left, top edge aligned with host element top edge.
+ *
left-bottom: element on left, bottom edge aligned with host element bottom edge.
+ *
right: element on right, vertically centered on host element.
+ *
right-top: element on right, top edge aligned with host element top edge.
+ *
right-bottom: element on right, bottom edge aligned with host element bottom edge.
+ *
+ * A placement string with an 'auto' indicator is expected to be
+ * space separated from the placement, i.e: 'auto bottom-left' If
+ * the primary and secondary placement values do not match 'top,
+ * bottom, left, right' then 'top' will be the primary placement and
+ * 'center' will be the secondary placement. If 'auto' is passed, true
+ * will be returned as the 3rd value of the array.
+ *
+ * @param {string} placement - The placement string to parse.
+ *
+ * @returns {array} An array with the following values
+ *
+ *
**[0]**: The primary placement.
+ *
**[1]**: The secondary placement.
+ *
**[2]**: If auto is passed: true, else undefined.
+ *
+ */
+ parsePlacement: function(placement) {
+ var autoPlace = PLACEMENT_REGEX.auto.test(placement);
+ if (autoPlace) {
+ placement = placement.replace(PLACEMENT_REGEX.auto, '');
+ }
+
+ placement = placement.split('-');
+
+ placement[0] = placement[0] || 'top';
+ if (!PLACEMENT_REGEX.primary.test(placement[0])) {
+ placement[0] = 'top';
+ }
+
+ placement[1] = placement[1] || 'center';
+ if (!PLACEMENT_REGEX.secondary.test(placement[1])) {
+ placement[1] = 'center';
+ }
+
+ if (autoPlace) {
+ placement[2] = true;
+ } else {
+ placement[2] = false;
+ }
+
+ return placement;
+ },
+
+ /**
+ * Provides coordinates for an element to be positioned relative to
+ * another element. Passing 'auto' as part of the placement parameter
+ * will enable smart placement - where the element fits. i.e:
+ * 'auto left-top' will check to see if there is enough space to the left
+ * of the hostElem to fit the targetElem, if not place right (same for secondary
+ * top placement). Available space is calculated using the viewportOffset
+ * function.
+ *
+ * @param {element} hostElem - The element to position against.
+ * @param {element} targetElem - The element to position.
+ * @param {string=} [placement=top] - The placement for the targetElem,
+ * default is 'top'. 'center' is assumed as secondary placement for
+ * 'top', 'left', 'right', and 'bottom' placements. Available placements are:
+ *
+ *
top
+ *
top-right
+ *
top-left
+ *
bottom
+ *
bottom-left
+ *
bottom-right
+ *
left
+ *
left-top
+ *
left-bottom
+ *
right
+ *
right-top
+ *
right-bottom
+ *
+ * @param {boolean=} [appendToBody=false] - Should the top and left values returned
+ * be calculated from the body element, default is false.
+ *
+ * @returns {object} An object with the following properties:
+ *
+ *
**top**: Value for targetElem top.
+ *
**left**: Value for targetElem left.
+ *
**placement**: The resolved placement.
+ *
+ */
+ positionElements: function(hostElem, targetElem, placement, appendToBody) {
+ hostElem = this.getRawNode(hostElem);
+ targetElem = this.getRawNode(targetElem);
+
+ // need to read from prop to support tests.
+ var targetWidth = angular.isDefined(targetElem.offsetWidth) ? targetElem.offsetWidth : targetElem.prop('offsetWidth');
+ var targetHeight = angular.isDefined(targetElem.offsetHeight) ? targetElem.offsetHeight : targetElem.prop('offsetHeight');
+
+ placement = this.parsePlacement(placement);
+
+ var hostElemPos = appendToBody ? this.offset(hostElem) : this.position(hostElem);
+ var targetElemPos = {top: 0, left: 0, placement: ''};
+
+ if (placement[2]) {
+ var viewportOffset = this.viewportOffset(hostElem, appendToBody);
- var shiftHeight = {
- center: function () {
- return hostElPos.top + hostElPos.height / 2 - targetElHeight / 2;
- },
- top: function () {
- return hostElPos.top;
- },
- bottom: function () {
- return hostElPos.top + hostElPos.height;
+ var targetElemStyle = $window.getComputedStyle(targetElem);
+ var adjustedSize = {
+ width: targetWidth + Math.round(Math.abs(this.parseStyle(targetElemStyle.marginLeft) + this.parseStyle(targetElemStyle.marginRight))),
+ height: targetHeight + Math.round(Math.abs(this.parseStyle(targetElemStyle.marginTop) + this.parseStyle(targetElemStyle.marginBottom)))
+ };
+
+ placement[0] = placement[0] === 'top' && adjustedSize.height > viewportOffset.top && adjustedSize.height <= viewportOffset.bottom ? 'bottom' :
+ placement[0] === 'bottom' && adjustedSize.height > viewportOffset.bottom && adjustedSize.height <= viewportOffset.top ? 'top' :
+ placement[0] === 'left' && adjustedSize.width > viewportOffset.left && adjustedSize.width <= viewportOffset.right ? 'right' :
+ placement[0] === 'right' && adjustedSize.width > viewportOffset.right && adjustedSize.width <= viewportOffset.left ? 'left' :
+ placement[0];
+
+ placement[1] = placement[1] === 'top' && adjustedSize.height - hostElemPos.height > viewportOffset.bottom && adjustedSize.height - hostElemPos.height <= viewportOffset.top ? 'bottom' :
+ placement[1] === 'bottom' && adjustedSize.height - hostElemPos.height > viewportOffset.top && adjustedSize.height - hostElemPos.height <= viewportOffset.bottom ? 'top' :
+ placement[1] === 'left' && adjustedSize.width - hostElemPos.width > viewportOffset.right && adjustedSize.width - hostElemPos.width <= viewportOffset.left ? 'right' :
+ placement[1] === 'right' && adjustedSize.width - hostElemPos.width > viewportOffset.left && adjustedSize.width - hostElemPos.width <= viewportOffset.right ? 'left' :
+ placement[1];
+
+ if (placement[1] === 'center') {
+ if (PLACEMENT_REGEX.vertical.test(placement[0])) {
+ var xOverflow = hostElemPos.width / 2 - targetWidth / 2;
+ if (viewportOffset.left + xOverflow < 0 && adjustedSize.width - hostElemPos.width <= viewportOffset.right) {
+ placement[1] = 'left';
+ } else if (viewportOffset.right + xOverflow < 0 && adjustedSize.width - hostElemPos.width <= viewportOffset.left) {
+ placement[1] = 'right';
+ }
+ } else {
+ var yOverflow = hostElemPos.height / 2 - adjustedSize.height / 2;
+ if (viewportOffset.top + yOverflow < 0 && adjustedSize.height - hostElemPos.height <= viewportOffset.bottom) {
+ placement[1] = 'top';
+ } else if (viewportOffset.bottom + yOverflow < 0 && adjustedSize.height - hostElemPos.height <= viewportOffset.top) {
+ placement[1] = 'bottom';
+ }
+ }
}
- };
+ }
- switch (pos0) {
+ switch (placement[0]) {
+ case 'top':
+ targetElemPos.top = hostElemPos.top - targetHeight;
+ break;
+ case 'bottom':
+ targetElemPos.top = hostElemPos.top + hostElemPos.height;
+ break;
+ case 'left':
+ targetElemPos.left = hostElemPos.left - targetWidth;
+ break;
case 'right':
- targetElPos = {
- top: shiftHeight[pos1](),
- left: shiftWidth[pos0]()
- };
+ targetElemPos.left = hostElemPos.left + hostElemPos.width;
+ break;
+ }
+
+ switch (placement[1]) {
+ case 'top':
+ targetElemPos.top = hostElemPos.top;
+ break;
+ case 'bottom':
+ targetElemPos.top = hostElemPos.top + hostElemPos.height - targetHeight;
break;
case 'left':
- targetElPos = {
- top: shiftHeight[pos1](),
- left: hostElPos.left - targetElWidth
- };
+ targetElemPos.left = hostElemPos.left;
+ break;
+ case 'right':
+ targetElemPos.left = hostElemPos.left + hostElemPos.width - targetWidth;
+ break;
+ case 'center':
+ if (PLACEMENT_REGEX.vertical.test(placement[0])) {
+ targetElemPos.left = hostElemPos.left + hostElemPos.width / 2 - targetWidth / 2;
+ } else {
+ targetElemPos.top = hostElemPos.top + hostElemPos.height / 2 - targetHeight / 2;
+ }
+ break;
+ }
+
+ targetElemPos.top = Math.round(targetElemPos.top);
+ targetElemPos.left = Math.round(targetElemPos.left);
+ targetElemPos.placement = placement[1] === 'center' ? placement[0] : placement[0] + '-' + placement[1];
+
+ return targetElemPos;
+ },
+
+ /**
+ * Provides a way to adjust the top positioning after first
+ * render to correctly align element to top after content
+ * rendering causes resized element height
+ *
+ * @param {array} placementClasses - The array of strings of classes
+ * element should have.
+ * @param {object} containerPosition - The object with container
+ * position information
+ * @param {number} initialHeight - The initial height for the elem.
+ * @param {number} currentHeight - The current height for the elem.
+ */
+ adjustTop: function(placementClasses, containerPosition, initialHeight, currentHeight) {
+ if (placementClasses.indexOf('top') !== -1 && initialHeight !== currentHeight) {
+ return {
+ top: containerPosition.top - currentHeight + 'px'
+ };
+ }
+ },
+
+ /**
+ * Provides a way for positioning tooltip & dropdown
+ * arrows when using placement options beyond the standard
+ * left, right, top, or bottom.
+ *
+ * @param {element} elem - The tooltip/dropdown element.
+ * @param {string} placement - The placement for the elem.
+ */
+ positionArrow: function(elem, placement) {
+ elem = this.getRawNode(elem);
+
+ var innerElem = elem.querySelector('.tooltip-inner, .popover-inner');
+ if (!innerElem) {
+ return;
+ }
+
+ var isTooltip = angular.element(innerElem).hasClass('tooltip-inner');
+
+ var arrowElem = isTooltip ? elem.querySelector('.tooltip-arrow') : elem.querySelector('.arrow');
+ if (!arrowElem) {
+ return;
+ }
+
+ var arrowCss = {
+ top: '',
+ bottom: '',
+ left: '',
+ right: ''
+ };
+
+ placement = this.parsePlacement(placement);
+ if (placement[1] === 'center') {
+ // no adjustment necessary - just reset styles
+ angular.element(arrowElem).css(arrowCss);
+ return;
+ }
+
+ var borderProp = 'border-' + placement[0] + '-width';
+ var borderWidth = $window.getComputedStyle(arrowElem)[borderProp];
+
+ var borderRadiusProp = 'border-';
+ if (PLACEMENT_REGEX.vertical.test(placement[0])) {
+ borderRadiusProp += placement[0] + '-' + placement[1];
+ } else {
+ borderRadiusProp += placement[1] + '-' + placement[0];
+ }
+ borderRadiusProp += '-radius';
+ var borderRadius = $window.getComputedStyle(isTooltip ? innerElem : elem)[borderRadiusProp];
+
+ switch (placement[0]) {
+ case 'top':
+ arrowCss.bottom = isTooltip ? '0' : '-' + borderWidth;
break;
case 'bottom':
- targetElPos = {
- top: shiftHeight[pos0](),
- left: shiftWidth[pos1]()
- };
+ arrowCss.top = isTooltip ? '0' : '-' + borderWidth;
break;
- default:
- targetElPos = {
- top: hostElPos.top - targetElHeight,
- left: shiftWidth[pos1]()
- };
+ case 'left':
+ arrowCss.right = isTooltip ? '0' : '-' + borderWidth;
+ break;
+ case 'right':
+ arrowCss.left = isTooltip ? '0' : '-' + borderWidth;
break;
}
- return targetElPos;
+ arrowCss[placement[1]] = borderRadius;
+
+ angular.element(arrowElem).css(arrowCss);
}
};
}]);
diff --git a/src/position/test/position.spec.js b/src/position/test/position.spec.js
index 96048193b2..fcc0ea83a3 100644
--- a/src/position/test/position.spec.js
+++ b/src/position/test/position.spec.js
@@ -1,5 +1,4 @@
-describe('position elements', function () {
-
+describe('$uibPosition service', function () {
var TargetElMock = function(width, height) {
this.width = width;
this.height = height;
@@ -9,12 +8,16 @@ describe('position elements', function () {
};
};
- var $position;
+ var $document;
+ var $uibPosition;
beforeEach(module('ui.bootstrap.position'));
- beforeEach(inject(function (_$position_) {
- $position = _$position_;
+
+ beforeEach(inject(function(_$document_, _$uibPosition_) {
+ $document = _$document_;
+ $uibPosition = _$uibPosition_;
}));
+
beforeEach(function () {
jasmine.addMatchers({
toBePositionedAt: function(util, customEqualityTesters) {
@@ -26,9 +29,9 @@ describe('position elements', function () {
};
if (result.pass) {
- result.message = 'Expected "(' + actual.top + ', ' + actual.left + ')" not to be positioned at (' + top + ', ' + left + ')';
+ result.message = 'Expected "(' + actual.top + ', ' + actual.left + ')" not to be positioned at (' + top + ', ' + left + ')';
} else {
- result.message = 'Expected "(' + actual.top + ', ' + actual.left + ')" to be positioned at (' + top + ', ' + left + ')';
+ result.message = 'Expected "(' + actual.top + ', ' + actual.left + ')" to be positioned at (' + top + ', ' + left + ')';
}
return result;
@@ -38,11 +41,357 @@ describe('position elements', function () {
});
});
- describe('append-to-body: false', function () {
+ describe('rawnode', function() {
+ it('returns the raw DOM element from an angular element', function() {
+ var angularEl = angular.element('');
+ var el = $uibPosition.getRawNode(angularEl);
+ expect(el.nodeName).toBe('DIV');
+ });
+
+ it('returns the raw DOM element from a select element', function() {
+ var angularEl = angular.element('');
+ var el = $uibPosition.getRawNode(angularEl);
+ expect(el.nodeName).toBe('SELECT');
+ });
+ });
+
+ describe('offset', function() {
+ it('returns getBoundingClientRect by default', function() {
+ var el = angular.element('
Foo
');
+
+ /* getBoundingClientRect values will be based on the testing Chrome window
+ so that makes this tests very brittle if we don't mock */
+ spyOn(el[0], 'getBoundingClientRect').and.returnValue({
+ width: 100,
+ height: 100,
+ top: 2,
+ left: 2
+ });
+ $document.find('body').append(el);
+
+ var offset = $uibPosition.offset(el);
+
+ expect(offset).toEqual({
+ width: 100,
+ height: 100,
+ top: 2,
+ left: 2
+ });
+
+ el.remove();
+ });
+ });
+
+ describe('viewportOffset', function() {
+ var el;
+
+ beforeEach(function() {
+ el = angular.element('
');
+
+ spyOn(el[0], 'getBoundingClientRect').and.returnValue({
+ width: 100,
+ height: 100,
+ top: 2,
+ left: 2
+ });
+
+ $document.find('body').append(el);
+
+ var position = $uibPosition.position(el);
+
+ expect(position).toEqual({
+ width: 100,
+ height: 100,
+ top: 2,
+ left: 2
+ });
+ });
+
+ it('gets position with an element as the relative parent', function() {
+ el = angular.element('
Foo
');
+
+ $document.find('body').append(el);
+
+ var outerEl = angular.element(document.getElementById('outer'));
+ var innerEl = angular.element(document.getElementById('inner'));
+
+ spyOn(outerEl[0], 'getBoundingClientRect').and.returnValue({
+ width: 100,
+ height: 100,
+ top: 2,
+ left: 2
+ });
+ spyOn(innerEl[0], 'getBoundingClientRect').and.returnValue({
+ width: 20,
+ height: 20,
+ top: 5,
+ left: 5
+ });
+
+ var position = $uibPosition.position(innerEl);
+
+ expect(position).toEqual({
+ width: 20,
+ height: 20,
+ top: 3,
+ left: 3
+ });
+ });
+ });
+
+ describe('isScrollable', function() {
+ var el;
+
+ afterEach(function() {
+ el.remove();
+ });
+
+ it('should return true if the element is scrollable', function() {
+ el = angular.element('');
+ $document.find('body').append(el);
+ expect($uibPosition.isScrollable(el)).toBe(true);
+ });
+
+ it('should return false if the element is scrollable', function() {
+ el = angular.element('');
+ $document.find('body').append(el);
+ expect($uibPosition.isScrollable(el)).toBe(false);
+ });
+
+ });
+
+ describe('scrollParent', function() {
+ var el;
+
+ afterEach(function() {
+ el.remove();
+ });
+
+ it('gets the closest scrollable ancestor', function() {
+ el = angular.element('
Foo
Bar
');
+
+ $document.find('body').css({overflow: 'auto'}).append(el);
+
+ var outerEl = document.getElementById('outer');
+ var innerEl = document.getElementById('inner');
+
+ var scrollParent = $uibPosition.scrollParent(innerEl);
+ expect(scrollParent).toEqual(outerEl);
+ });
+
+ it('gets the closest scrollable ancestor with overflow-x: scroll', function() {
+ el = angular.element('
Foo
Bar
');
+
+ $document.find('body').css({overflow: 'auto'}).append(el);
+
+ var outerEl = document.getElementById('outer');
+ var innerEl = document.getElementById('inner');
+
+ var scrollParent = $uibPosition.scrollParent(innerEl);
+ expect(scrollParent).toEqual(outerEl);
+ });
+
+ it('gets the closest scrollable ancestor with overflow-y: hidden', function() {
+ el = angular.element('
Foo
Bar
');
+
+ $document.find('body').css({overflow: 'auto'}).append(el);
+
+ var outerEl = document.getElementById('outer');
+ var innerEl = document.getElementById('inner');
+
+ var scrollParent = $uibPosition.scrollParent(innerEl, true);
+ expect(scrollParent).toEqual(outerEl);
+ });
- beforeEach(function () {
+ it('gets the document element if no scrollable ancestor exists', function() {
+ el = angular.element('
Foo
Bar
');
+
+ $document.find('body').css({overflow: ''}).append(el);
+
+ var innerEl = document.getElementById('inner');
+
+ var scrollParent = $uibPosition.scrollParent(innerEl);
+ expect(scrollParent).toEqual($document[0].documentElement);
+ });
+
+ it('gets the closest scrollable ancestor after a positioned ancestor when positioned absolute', function() {
+ el = angular.element('
Foo
Bar
');
+
+ $document.find('body').css({overflow: 'auto'}).append(el);
+
+ var outerEl = document.getElementById('outer');
+ var innerEl = document.getElementById('inner');
+
+ var scrollParent = $uibPosition.scrollParent(innerEl);
+ expect(scrollParent).toEqual(outerEl);
+ });
+ });
+
+ describe('positionElements - append-to-body: false', function() {
+ var el;
+
+ beforeEach(function() {
//mock position info normally queried from the DOM
- $position.position = function() {
+ $uibPosition.position = function() {
+ return {
+ width: 20,
+ height: 20,
+ top: 100,
+ left: 100
+ };
+ };
+ });
+
+ it('should position element on top-center by default', function() {
+ expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'other')).toBePositionedAt(90, 105);
+ expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'top')).toBePositionedAt(90, 105);
+ expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'top-center')).toBePositionedAt(90, 105);
+ });
+
+ it('should position on top-left', function() {
+ expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'top-left')).toBePositionedAt(90, 100);
+ });
+
+ it('should position on top-right', function() {
+ expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'top-right')).toBePositionedAt(90, 110);
+ });
+
+ it('should position elements on bottom-center when "bottom" specified', function() {
+ expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'bottom')).toBePositionedAt(120, 105);
+ expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'bottom-center')).toBePositionedAt(120, 105);
+ });
+
+ it('should position elements on bottom-left', function() {
+ expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'bottom-left')).toBePositionedAt(120, 100);
+ });
+
+ it('should position elements on bottom-right', function() {
+ expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'bottom-right')).toBePositionedAt(120, 110);
+ });
+
+ it('should position elements on left-center when "left" specified', function() {
+ expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'left')).toBePositionedAt(105, 90);
+ expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'left-center')).toBePositionedAt(105, 90);
+ });
+
+ it('should position elements on left-top when "left-top" specified', function() {
+ expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'left-top')).toBePositionedAt(100, 90);
+ });
+
+ it('should position elements on left-bottom when "left-bottom" specified', function() {
+ expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'left-bottom')).toBePositionedAt(110, 90);
+ });
+
+ it('should position elements on right-center when "right" specified', function() {
+ expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'right')).toBePositionedAt(105, 120);
+ expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'right-center')).toBePositionedAt(105, 120);
+ });
+
+ it('should position elements on right-top when "right-top" specified', function() {
+ expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'right-top')).toBePositionedAt(100, 120);
+ });
+
+ it('should position elements on right-top when "right-top" specified', function() {
+ expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'right-bottom')).toBePositionedAt(110, 120);
+ });
+ });
+
+ describe('positionElements - append-to-body: true', function() {
+ beforeEach(function() {
+ //mock offset info normally queried from the DOM
+ $uibPosition.offset = function() {
return {
width: 20,
height: 20,
@@ -52,58 +401,157 @@ describe('position elements', function () {
};
});
- it('should position element on top-center by default', function () {
- expect($position.positionElements({}, new TargetElMock(10, 10), 'other')).toBePositionedAt(90, 105);
- expect($position.positionElements({}, new TargetElMock(10, 10), 'top')).toBePositionedAt(90, 105);
- expect($position.positionElements({}, new TargetElMock(10, 10), 'top-center')).toBePositionedAt(90, 105);
+ it('should position element on top-center by default', function() {
+ expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'other', true)).toBePositionedAt(90, 105);
+ expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'top', true)).toBePositionedAt(90, 105);
+ expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'top-center', true)).toBePositionedAt(90, 105);
});
- it('should position on top-left', function () {
- expect($position.positionElements({}, new TargetElMock(10, 10), 'top-left')).toBePositionedAt(90, 100);
+ it('should position on top-left', function() {
+ expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'top-left', true)).toBePositionedAt(90, 100);
});
- it('should position on top-right', function () {
- expect($position.positionElements({}, new TargetElMock(10, 10), 'top-right')).toBePositionedAt(90, 120);
+ it('should position on top-right', function() {
+ expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'top-right', true)).toBePositionedAt(90, 110);
});
- it('should position elements on bottom-center when "bottom" specified', function () {
- expect($position.positionElements({}, new TargetElMock(10, 10), 'bottom')).toBePositionedAt(120, 105);
- expect($position.positionElements({}, new TargetElMock(10, 10), 'bottom-center')).toBePositionedAt(120, 105);
+ it('should position elements on bottom-center when "bottom" specified', function() {
+ expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'bottom', true)).toBePositionedAt(120, 105);
+ expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'bottom-center', true)).toBePositionedAt(120, 105);
});
- it('should position elements on bottom-left', function () {
- expect($position.positionElements({}, new TargetElMock(10, 10), 'bottom-left')).toBePositionedAt(120, 100);
+ it('should position elements on bottom-left', function() {
+ expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'bottom-left', true)).toBePositionedAt(120, 100);
});
- it('should position elements on bottom-right', function () {
- expect($position.positionElements({}, new TargetElMock(10, 10), 'bottom-right')).toBePositionedAt(120, 120);
+ it('should position elements on bottom-right', function() {
+ expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'bottom-right', true)).toBePositionedAt(120, 110);
});
- it('should position elements on left-center when "left" specified', function () {
- expect($position.positionElements({}, new TargetElMock(10, 10), 'left')).toBePositionedAt(105, 90);
- expect($position.positionElements({}, new TargetElMock(10, 10), 'left-center')).toBePositionedAt(105, 90);
+ it('should position elements on left-center when "left" specified', function() {
+ expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'left', true)).toBePositionedAt(105, 90);
+ expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'left-center', true)).toBePositionedAt(105, 90);
});
- it('should position elements on left-top when "left-top" specified', function () {
- expect($position.positionElements({}, new TargetElMock(10, 10), 'left-top')).toBePositionedAt(100, 90);
+ it('should position elements on left-top when "left-top" specified', function() {
+ expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'left-top', true)).toBePositionedAt(100, 90);
});
- it('should position elements on left-bottom when "left-bottom" specified', function () {
- expect($position.positionElements({}, new TargetElMock(10, 10), 'left-bottom')).toBePositionedAt(120, 90);
+ it('should position elements on left-bottom when "left-bottom" specified', function() {
+ expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'left-bottom', true)).toBePositionedAt(110, 90);
});
- it('should position elements on right-center when "right" specified', function () {
- expect($position.positionElements({}, new TargetElMock(10, 10), 'right')).toBePositionedAt(105, 120);
- expect($position.positionElements({}, new TargetElMock(10, 10), 'right-center')).toBePositionedAt(105, 120);
+ it('should position elements on right-center when "right" specified', function() {
+ expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'right', true)).toBePositionedAt(105, 120);
+ expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'right-center', true)).toBePositionedAt(105, 120);
});
- it('should position elements on right-top when "right-top" specified', function () {
- expect($position.positionElements({}, new TargetElMock(10, 10), 'right-top')).toBePositionedAt(100, 120);
+ it('should position elements on right-top when "right-top" specified', function() {
+ expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'right-top', true)).toBePositionedAt(100, 120);
});
- it('should position elements on right-top when "right-top" specified', function () {
- expect($position.positionElements({}, new TargetElMock(10, 10), 'right-bottom')).toBePositionedAt(120, 120);
+ it('should position elements on right-bottom when "right-bottom" specified', function() {
+ expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'right-bottom', true)).toBePositionedAt(110, 120);
});
});
+ describe('smart positioning', function() {
+ var viewportOffset, el;
+
+ beforeEach(function() {
+ el = angular.element('');
+ $document.find('body').append(el);
+
+ //mock position info normally queried from the DOM
+ $uibPosition.position = function() {
+ return {
+ width: 40,
+ height: 40,
+ top: 100,
+ left: 100
+ };
+ };
+
+ viewportOffset = {
+ width: 10,
+ height: 10,
+ top: 10,
+ bottom: 10,
+ left: 10,
+ right: 10
+ };
+
+ $uibPosition.viewportOffset = function() {
+ return viewportOffset;
+ };
+ });
+
+ afterEach(function() {
+ el.remove();
+ });
+
+ // tests primary top -> bottom
+ // tests secondary left -> right
+ it('should position element on bottom-right when top-left does not fit', function() {
+ viewportOffset.bottom = 20;
+ viewportOffset.left = 20;
+ el.css({ width: '60px', height: '20px' });
+ expect($uibPosition.positionElements({}, el, 'auto top-left')).toBePositionedAt(140, 80);
+ });
+
+ // tests primary bottom -> top
+ // tests secondary right -> left
+ it('should position element on top-left when bottom-right does not fit', function() {
+ viewportOffset.top = 20;
+ viewportOffset.right = 20;
+ el.css({ width: '60px', height: '20px' });
+ expect($uibPosition.positionElements({}, el, 'auto bottom-right')).toBePositionedAt(80, 100);
+ });
+
+ // tests primary left -> right
+ // tests secondary top -> bottom
+ it('should position element on right-bottom when left-top does not fit', function() {
+ viewportOffset.top = 20;
+ viewportOffset.right = 20;
+ el.css({ width: '20px', height: '60px' });
+ expect($uibPosition.positionElements({}, el, 'auto left-top')).toBePositionedAt(80, 140);
+ });
+
+ // tests primary right -> left
+ // tests secondary bottom -> top
+ it('should position element on left-top when right-bottom does not fit', function() {
+ viewportOffset.bottom = 20;
+ viewportOffset.left = 20;
+ el.css({ width: '20px', height: '60px' });
+ expect($uibPosition.positionElements({}, el, 'auto right-bottom')).toBePositionedAt(100, 80);
+ });
+
+ // tests vertical center -> top
+ it('should position element on left-top when left-center does not fit vetically', function() {
+ viewportOffset.bottom = 100;
+ el.css({ width: '20px', height: '120px' });
+ expect($uibPosition.positionElements({}, el, 'auto left')).toBePositionedAt(100, 80);
+ });
+
+ // tests vertical center -> bottom
+ it('should position element on left-bottom when left-center does not fit vertically', function() {
+ viewportOffset.top = 100;
+ el.css({ width: '20px', height: '120px' });
+ expect($uibPosition.positionElements({}, el, 'auto left')).toBePositionedAt(20, 80);
+ });
+
+ // tests horizontal center -> left
+ it('should position element on top-left when top-center does not fit horizontally', function() {
+ viewportOffset.right = 100;
+ el.css({ width: '120px', height: '20px' });
+ expect($uibPosition.positionElements({}, el, 'auto top')).toBePositionedAt(80, 100);
+ });
+
+ // tests horizontal center -> right
+ it('should position element on top-right when top-center does not fit horizontally', function() {
+ viewportOffset.left = 100;
+ el.css({ width: '120px', height: '20px' });
+ expect($uibPosition.positionElements({}, el, 'auto top')).toBePositionedAt(80, 20);
+ });
+ });
});
diff --git a/src/progressbar/docs/demo.html b/src/progressbar/docs/demo.html
index f6727b06c0..3073be7c1b 100644
--- a/src/progressbar/docs/demo.html
+++ b/src/progressbar/docs/demo.html
@@ -2,23 +2,23 @@
Static
-
-
22%
-
166 / 200
+
+
22%
+
166 / 200
-
Dynamic Randomize
- {{dynamic}} / {{max}}
+
Dynamic Randomize
+ {{dynamic}} / {{max}}No animation
- {{dynamic}}%
+ {{dynamic}}%Object (changes type based on value)
- {{type}} !!! Watch out !!!
+ {{type}} !!! Watch out !!!
-
Stacked Randomize
-
+
Stacked Randomize
+ {{bar.value}}%
\ No newline at end of file
diff --git a/src/progressbar/docs/demo.js b/src/progressbar/docs/demo.js
index c06561ca2d..10f4dc5778 100644
--- a/src/progressbar/docs/demo.js
+++ b/src/progressbar/docs/demo.js
@@ -2,7 +2,7 @@ angular.module('ui.bootstrap.demo').controller('ProgressDemoCtrl', function ($sc
$scope.max = 200;
$scope.random = function() {
- var value = Math.floor((Math.random() * 100) + 1);
+ var value = Math.floor(Math.random() * 100 + 1);
var type;
if (value < 25) {
@@ -15,24 +15,26 @@ angular.module('ui.bootstrap.demo').controller('ProgressDemoCtrl', function ($sc
type = 'danger';
}
- $scope.showWarning = (type === 'danger' || type === 'warning');
+ $scope.showWarning = type === 'danger' || type === 'warning';
$scope.dynamic = value;
$scope.type = type;
};
+
$scope.random();
$scope.randomStacked = function() {
$scope.stacked = [];
var types = ['success', 'info', 'warning', 'danger'];
- for (var i = 0, n = Math.floor((Math.random() * 4) + 1); i < n; i++) {
- var index = Math.floor((Math.random() * 4));
+ for (var i = 0, n = Math.floor(Math.random() * 4 + 1); i < n; i++) {
+ var index = Math.floor(Math.random() * 4);
$scope.stacked.push({
- value: Math.floor((Math.random() * 30) + 1),
+ value: Math.floor(Math.random() * 30 + 1),
type: types[index]
});
}
};
+
$scope.randomStacked();
});
diff --git a/src/progressbar/docs/readme.md b/src/progressbar/docs/readme.md
index a50766d11b..ee0956964d 100644
--- a/src/progressbar/docs/readme.md
+++ b/src/progressbar/docs/readme.md
@@ -1,29 +1,65 @@
A progress bar directive that is focused on providing feedback on the progress of a workflow or action.
-It supports multiple (stacked) bars into the same `
diff --git a/src/rating/docs/readme.md b/src/rating/docs/readme.md
index 0cacddd422..1b27d15206 100644
--- a/src/rating/docs/readme.md
+++ b/src/rating/docs/readme.md
@@ -1,37 +1,56 @@
Rating directive that will take care of visualising a star rating bar.
-### Settings ###
-
-#### `` ####
-
- * `ng-model`
- :
- The current rate.
-
- * `max`
- _(Defaults: 5)_ :
- Changes the number of icons.
-
- * `readonly`
- _(Defaults: false)_ :
- Prevent user's interaction.
-
- * `on-hover(value)`
- :
- An optional expression called when user's mouse is over a particular icon.
-
- * `on-leave()`
- :
- An optional expression called when user's mouse leaves the control altogether.
-
- * `state-on`
- _(Defaults: null)_ :
- A variable used in template to specify the state (class, src, etc) for selected icons.
-
- * `state-off`
- _(Defaults: null)_ :
- A variable used in template to specify the state for unselected icons.
-
- * `rating-states`
- _(Defaults: null)_ :
- An array of objects defining properties for all icons. In default template, `stateOn` & `stateOff` property is used to specify the icon's class.
+### uib-rating settings
+
+* `max`
+ $
+ C
+ _(Default: `5`)_ -
+ Changes the number of icons.
+
+* `ng-model`
+ $
+ -
+ The current rate.
+
+* `on-hover(value)`
+ $ -
+ An optional expression called when user's mouse is over a particular icon.
+
+* `on-leave()`
+ $ -
+ An optional expression called when user's mouse leaves the control altogether.
+
+* `rating-states`
+ $
+ _(Default: `null`)_ -
+ An array of objects defining properties for all icons. In default template, `stateOn` & `stateOff` property is used to specify the icon's class.
+
+* `read-only`
+ $
+
+ _(Default: `false`)_ -
+ Prevent user's interaction.
+
+* `titles`
+ $
+ C
+ _(Default: ['one', 'two', 'three', 'four', 'five']`)_ -
+ An array of strings defining titles for all icons.
+
+* `enable-reset`
+ $
+ _(Default: `true`)_ -
+ Clicking the icon of the current rating will reset the rating to 0.
+
+* `state-off`
+ $
+ C
+ _(Default: `null`)_ -
+ A variable used in the template to specify the state for unselected icons.
+
+* `state-on`
+ $
+ C
+ _(Default: `null`)_ -
+ A variable used in the template to specify the state (class, src, etc) for selected icons.
diff --git a/src/rating/index.js b/src/rating/index.js
new file mode 100644
index 0000000000..8d7aa8751b
--- /dev/null
+++ b/src/rating/index.js
@@ -0,0 +1,8 @@
+require('../../template/rating/rating.html.js');
+require('./rating');
+
+var MODULE_NAME = 'ui.bootstrap.module.rating';
+
+angular.module(MODULE_NAME, ['ui.bootstrap.rating', 'uib/template/rating/rating.html']);
+
+module.exports = MODULE_NAME;
diff --git a/src/rating/rating.js b/src/rating/rating.js
index 86327c87a3..831c515432 100644
--- a/src/rating/rating.js
+++ b/src/rating/rating.js
@@ -1,13 +1,16 @@
angular.module('ui.bootstrap.rating', [])
-.constant('ratingConfig', {
+.constant('uibRatingConfig', {
max: 5,
stateOn: null,
- stateOff: null
+ stateOff: null,
+ enableReset: true,
+ titles: ['one', 'two', 'three', 'four', 'five']
})
-.controller('RatingController', ['$scope', '$attrs', 'ratingConfig', function($scope, $attrs, ratingConfig) {
- var ngModelCtrl = { $setViewValue: angular.noop };
+.controller('UibRatingController', ['$scope', '$attrs', 'uibRatingConfig', function($scope, $attrs, ratingConfig) {
+ var ngModelCtrl = { $setViewValue: angular.noop },
+ self = this;
this.init = function(ngModelCtrl_) {
ngModelCtrl = ngModelCtrl_;
@@ -17,33 +20,49 @@ angular.module('ui.bootstrap.rating', [])
if (angular.isNumber(value) && value << 0 !== value) {
value = Math.round(value);
}
+
return value;
});
this.stateOn = angular.isDefined($attrs.stateOn) ? $scope.$parent.$eval($attrs.stateOn) : ratingConfig.stateOn;
this.stateOff = angular.isDefined($attrs.stateOff) ? $scope.$parent.$eval($attrs.stateOff) : ratingConfig.stateOff;
+ this.enableReset = angular.isDefined($attrs.enableReset) ?
+ $scope.$parent.$eval($attrs.enableReset) : ratingConfig.enableReset;
+ var tmpTitles = angular.isDefined($attrs.titles) ? $scope.$parent.$eval($attrs.titles) : ratingConfig.titles;
+ this.titles = angular.isArray(tmpTitles) && tmpTitles.length > 0 ?
+ tmpTitles : ratingConfig.titles;
- var ratingStates = angular.isDefined($attrs.ratingStates) ? $scope.$parent.$eval($attrs.ratingStates) :
- new Array( angular.isDefined($attrs.max) ? $scope.$parent.$eval($attrs.max) : ratingConfig.max );
+ var ratingStates = angular.isDefined($attrs.ratingStates) ?
+ $scope.$parent.$eval($attrs.ratingStates) :
+ new Array(angular.isDefined($attrs.max) ? $scope.$parent.$eval($attrs.max) : ratingConfig.max);
$scope.range = this.buildTemplateObjects(ratingStates);
};
this.buildTemplateObjects = function(states) {
for (var i = 0, n = states.length; i < n; i++) {
- states[i] = angular.extend({ index: i }, { stateOn: this.stateOn, stateOff: this.stateOff }, states[i]);
+ states[i] = angular.extend({ index: i }, { stateOn: this.stateOn, stateOff: this.stateOff, title: this.getTitle(i) }, states[i]);
}
return states;
};
+ this.getTitle = function(index) {
+ if (index >= this.titles.length) {
+ return index + 1;
+ }
+
+ return this.titles[index];
+ };
+
$scope.rate = function(value) {
- if ( !$scope.readonly && value >= 0 && value <= $scope.range.length ) {
- ngModelCtrl.$setViewValue(value);
+ if (!$scope.readonly && value >= 0 && value <= $scope.range.length) {
+ var newViewValue = self.enableReset && ngModelCtrl.$viewValue === value ? 0 : value;
+ ngModelCtrl.$setViewValue(newViewValue);
ngModelCtrl.$render();
}
};
$scope.enter = function(value) {
- if ( !$scope.readonly ) {
+ if (!$scope.readonly) {
$scope.value = value;
}
$scope.onHover({value: value});
@@ -58,30 +77,30 @@ angular.module('ui.bootstrap.rating', [])
if (/(37|38|39|40)/.test(evt.which)) {
evt.preventDefault();
evt.stopPropagation();
- $scope.rate( $scope.value + (evt.which === 38 || evt.which === 39 ? 1 : -1) );
+ $scope.rate($scope.value + (evt.which === 38 || evt.which === 39 ? 1 : -1));
}
};
this.render = function() {
$scope.value = ngModelCtrl.$viewValue;
+ $scope.title = self.getTitle($scope.value - 1);
};
}])
-.directive('rating', function() {
+.directive('uibRating', function() {
return {
- restrict: 'EA',
- require: ['rating', 'ngModel'],
+ require: ['uibRating', 'ngModel'],
+ restrict: 'A',
scope: {
- readonly: '=?',
+ readonly: '=?readOnly',
onHover: '&',
onLeave: '&'
},
- controller: 'RatingController',
- templateUrl: 'template/rating/rating.html',
- replace: true,
+ controller: 'UibRatingController',
+ templateUrl: 'uib/template/rating/rating.html',
link: function(scope, element, attrs, ctrls) {
var ratingCtrl = ctrls[0], ngModelCtrl = ctrls[1];
- ratingCtrl.init( ngModelCtrl );
+ ratingCtrl.init(ngModelCtrl);
}
};
-});
\ No newline at end of file
+});
diff --git a/src/rating/test/rating.spec.js b/src/rating/test/rating.spec.js
index 038bdd4f39..6e2b3e85a6 100644
--- a/src/rating/test/rating.spec.js
+++ b/src/rating/test/rating.spec.js
@@ -1,46 +1,55 @@
-describe('rating directive', function () {
- var $rootScope, $compile, element;
+describe('rating directive', function() {
+ var $rootScope, $compile, element, innerElem;
beforeEach(module('ui.bootstrap.rating'));
- beforeEach(module('template/rating/rating.html'));
+ beforeEach(module('uib/template/rating/rating.html'));
beforeEach(inject(function(_$compile_, _$rootScope_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
$rootScope.rate = 3;
- element = $compile('')($rootScope);
+ element = $compile('')($rootScope);
$rootScope.$digest();
+ innerElem = element.children().eq(0);
}));
function getStars() {
- return element.find('i');
+ return innerElem.find('i');
}
function getStar(number) {
- return getStars().eq( number - 1 );
+ return getStars().eq(number - 1);
}
function getState(classOn, classOff) {
var stars = getStars();
var state = [];
for (var i = 0, n = stars.length; i < n; i++) {
- state.push( (stars.eq(i).hasClass(classOn || 'glyphicon-star') && ! stars.eq(i).hasClass(classOff || 'glyphicon-star-empty')) );
+ state.push(stars.eq(i).hasClass(classOn || 'glyphicon-star') &&
+ !stars.eq(i).hasClass(classOff || 'glyphicon-star-empty'));
}
return state;
}
+ function getTitles() {
+ var stars = getStars();
+ return stars.toArray().map(function(star) {
+ return angular.element(star).attr('title');
+ });
+ }
+
function triggerKeyDown(keyCode) {
var e = $.Event('keydown');
e.which = keyCode;
- element.trigger(e);
+ innerElem.trigger(e);
}
it('contains the default number of icons', function() {
expect(getStars().length).toBe(5);
- expect(element.attr('aria-valuemax')).toBe('5');
+ expect(innerElem.attr('aria-valuemax')).toBe('5');
});
it('initializes the default star icons as selected', function() {
expect(getState()).toEqual([true, true, true, false, false]);
- expect(element.attr('aria-valuenow')).toBe('3');
+ expect(innerElem.attr('aria-valuenow')).toBe('3');
});
it('handles correctly the click event', function() {
@@ -48,13 +57,19 @@ describe('rating directive', function () {
$rootScope.$digest();
expect(getState()).toEqual([true, true, false, false, false]);
expect($rootScope.rate).toBe(2);
- expect(element.attr('aria-valuenow')).toBe('2');
+ expect(innerElem.attr('aria-valuenow')).toBe('2');
getStar(5).click();
$rootScope.$digest();
expect(getState()).toEqual([true, true, true, true, true]);
expect($rootScope.rate).toBe(5);
- expect(element.attr('aria-valuenow')).toBe('5');
+ expect(innerElem.attr('aria-valuenow')).toBe('5');
+
+ getStar(5).click();
+ $rootScope.$digest();
+ expect(getState()).toEqual([false, false, false, false, false]);
+ expect($rootScope.rate).toBe(0);
+ expect(innerElem.attr('aria-valuenow')).toBe('0');
});
it('handles correctly the hover event', function() {
@@ -68,7 +83,7 @@ describe('rating directive', function () {
expect(getState()).toEqual([true, true, true, true, true]);
expect($rootScope.rate).toBe(3);
- element.trigger('mouseout');
+ innerElem.trigger('mouseout');
expect(getState()).toEqual([true, true, true, false, false]);
expect($rootScope.rate).toBe(3);
});
@@ -78,13 +93,13 @@ describe('rating directive', function () {
$rootScope.$digest();
expect(getState()).toEqual([true, true, false, false, false]);
- expect(element.attr('aria-valuenow')).toBe('2');
+ expect(innerElem.attr('aria-valuenow')).toBe('2');
$rootScope.rate = 2.5;
$rootScope.$digest();
expect(getState()).toEqual([true, true, true, false, false]);
- expect(element.attr('aria-valuenow')).toBe('3');
+ expect(innerElem.attr('aria-valuenow')).toBe('3');
});
it('changes the number of selected icons when value changes', function() {
@@ -92,29 +107,33 @@ describe('rating directive', function () {
$rootScope.$digest();
expect(getState()).toEqual([true, true, false, false, false]);
- expect(element.attr('aria-valuenow')).toBe('2');
+ expect(innerElem.attr('aria-valuenow')).toBe('2');
+ expect(innerElem.attr('aria-valuetext')).toBe('two');
});
it('shows different number of icons when `max` attribute is set', function() {
- element = $compile('')($rootScope);
+ element = $compile('')($rootScope);
$rootScope.$digest();
+ innerElem = element.children().eq(0);
expect(getStars().length).toBe(7);
- expect(element.attr('aria-valuemax')).toBe('7');
+ expect(innerElem.attr('aria-valuemax')).toBe('7');
});
it('shows different number of icons when `max` attribute is from scope variable', function() {
$rootScope.max = 15;
- element = $compile('')($rootScope);
+ element = $compile('')($rootScope);
$rootScope.$digest();
+ innerElem = element.children().eq(0);
expect(getStars().length).toBe(15);
- expect(element.attr('aria-valuemax')).toBe('15');
+ expect(innerElem.attr('aria-valuemax')).toBe('15');
});
- it('handles readonly attribute', function() {
+ it('handles read-only attribute', function() {
$rootScope.isReadonly = true;
- element = $compile('')($rootScope);
+ element = $compile('')($rootScope);
$rootScope.$digest();
+ innerElem = element.children().eq(0);
expect(getState()).toEqual([true, true, true, false, false]);
@@ -131,10 +150,37 @@ describe('rating directive', function () {
expect(getState()).toEqual([true, true, true, true, true]);
});
+ it('handles enable-reset attribute', function() {
+ $rootScope.canReset = false;
+ element = $compile('')($rootScope);
+ $rootScope.$digest();
+ innerElem = element.children().eq(0);
+
+ var star = {
+ states: [true, true, true, true, true],
+ rating: 5
+ };
+
+ var selectStar = getStar(star.rating);
+
+ selectStar.click();
+ $rootScope.$digest();
+ expect(getState()).toEqual(star.states);
+ expect($rootScope.rate).toBe(5);
+ expect(innerElem.attr('aria-valuenow')).toBe('5');
+
+ selectStar.click();
+ $rootScope.$digest();
+ expect(getState()).toEqual(star.states);
+ expect($rootScope.rate).toBe(5);
+ expect(innerElem.attr('aria-valuenow')).toBe('5');
+ });
+
it('should fire onHover', function() {
$rootScope.hoveringOver = jasmine.createSpy('hoveringOver');
- element = $compile('')($rootScope);
+ element = $compile('')($rootScope);
$rootScope.$digest();
+ innerElem = element.children().eq(0);
getStar(3).trigger('mouseover');
$rootScope.$digest();
@@ -143,10 +189,11 @@ describe('rating directive', function () {
it('should fire onLeave', function() {
$rootScope.leaving = jasmine.createSpy('leaving');
- element = $compile('')($rootScope);
+ element = $compile('')($rootScope);
$rootScope.$digest();
+ innerElem = element.children().eq(0);
- element.trigger('mouseleave');
+ innerElem.trigger('mouseleave');
$rootScope.$digest();
expect($rootScope.leaving).toHaveBeenCalled();
});
@@ -203,8 +250,9 @@ describe('rating directive', function () {
beforeEach(inject(function() {
$rootScope.classOn = 'icon-ok-sign';
$rootScope.classOff = 'icon-ok-circle';
- element = $compile('')($rootScope);
+ element = $compile('')($rootScope);
$rootScope.$digest();
+ innerElem = element.children().eq(0);
}));
it('changes the default icons', function() {
@@ -220,13 +268,14 @@ describe('rating directive', function () {
{stateOn: 'heart'},
{stateOff: 'off'}
];
- element = $compile('')($rootScope);
+ element = $compile('')($rootScope);
$rootScope.$digest();
+ innerElem = element.children().eq(0);
}));
- it('should define number of icon elements', function () {
+ it('should define number of icon elements', function() {
expect(getStars().length).toBe(4);
- expect(element.attr('aria-valuemax')).toBe('4');
+ expect(innerElem.attr('aria-valuemax')).toBe('4');
});
it('handles each icon', function() {
@@ -243,28 +292,77 @@ describe('rating directive', function () {
});
});
- describe('setting ratingConfig', function() {
+ describe('setting uibRatingConfig', function() {
var originalConfig = {};
- beforeEach(inject(function(ratingConfig) {
+ beforeEach(inject(function(uibRatingConfig) {
$rootScope.rate = 5;
- angular.extend(originalConfig, ratingConfig);
- ratingConfig.max = 10;
- ratingConfig.stateOn = 'on';
- ratingConfig.stateOff = 'off';
- element = $compile('')($rootScope);
+ angular.extend(originalConfig, uibRatingConfig);
+ uibRatingConfig.max = 10;
+ uibRatingConfig.stateOn = 'on';
+ uibRatingConfig.stateOff = 'off';
+ element = $compile('')($rootScope);
$rootScope.$digest();
+ innerElem = element.children().eq(0);
}));
- afterEach(inject(function(ratingConfig) {
+ afterEach(inject(function(uibRatingConfig) {
// return it to the original state
- angular.extend(ratingConfig, originalConfig);
+ angular.extend(uibRatingConfig, originalConfig);
}));
- it('should change number of icon elements', function () {
+ it('should change number of icon elements', function() {
expect(getStars().length).toBe(10);
});
- it('should change icon states', function () {
+ it('should change icon states', function() {
expect(getState('on', 'off')).toEqual([true, true, true, true, true, false, false, false, false, false]);
});
});
+
+ describe('Default title', function() {
+ it('should return the default title for each star', function() {
+ expect(getTitles()).toEqual(['one', 'two', 'three', 'four', 'five']);
+ });
+ });
+
+ describe('shows different title when `max` attribute is greater than the titles array ', function() {
+ var originalConfig = {};
+ beforeEach(inject(function(uibRatingConfig) {
+ $rootScope.rate = 5;
+ angular.extend(originalConfig, uibRatingConfig);
+ uibRatingConfig.max = 10;
+ element = $compile('')($rootScope);
+ $rootScope.$digest();
+ innerElem = element.children().eq(0);
+ }));
+ afterEach(inject(function(uibRatingConfig) {
+ // return it to the original state
+ angular.extend(uibRatingConfig, originalConfig);
+ }));
+
+ it('should return the default title for each star', function() {
+ expect(getTitles()).toEqual(['one', 'two', 'three', 'four', 'five', '6', '7', '8', '9', '10']);
+ });
+ });
+
+ describe('shows custom titles ', function() {
+ it('should return the custom title for each star', function() {
+ $rootScope.titles = [44,45,46];
+ element = $compile('')($rootScope);
+ $rootScope.$digest();
+ innerElem = element.children().eq(0);
+ expect(getTitles()).toEqual(['44', '45', '46', '4', '5']);
+ });
+ it('should return the default title if the custom title is empty', function() {
+ $rootScope.titles = [];
+ element = $compile('')($rootScope);
+ $rootScope.$digest();
+ innerElem = element.children().eq(0);
+ expect(getTitles()).toEqual(['one', 'two', 'three', 'four', 'five']);
+ });
+ it('should return the default title if the custom title is not an array', function() {
+ element = $compile('')($rootScope);
+ $rootScope.$digest();
+ expect(getTitles()).toEqual(['one', 'two', 'three', 'four', 'five']);
+ });
+ });
});
diff --git a/src/stackedMap/index.js b/src/stackedMap/index.js
new file mode 100644
index 0000000000..85b9b4a65d
--- /dev/null
+++ b/src/stackedMap/index.js
@@ -0,0 +1,7 @@
+require('./stackedMap');
+
+var MODULE_NAME = 'ui.bootstrap.module.stackedMap';
+
+angular.module(MODULE_NAME, ['ui.bootstrap.stackedMap']);
+
+module.exports = MODULE_NAME;
diff --git a/src/stackedMap/stackedMap.js b/src/stackedMap/stackedMap.js
new file mode 100644
index 0000000000..f1f4d857b4
--- /dev/null
+++ b/src/stackedMap/stackedMap.js
@@ -0,0 +1,54 @@
+angular.module('ui.bootstrap.stackedMap', [])
+/**
+ * A helper, internal data structure that acts as a map but also allows getting / removing
+ * elements in the LIFO order
+ */
+ .factory('$$stackedMap', function() {
+ return {
+ createNew: function() {
+ var stack = [];
+
+ return {
+ add: function(key, value) {
+ stack.push({
+ key: key,
+ value: value
+ });
+ },
+ get: function(key) {
+ for (var i = 0; i < stack.length; i++) {
+ if (key === stack[i].key) {
+ return stack[i];
+ }
+ }
+ },
+ keys: function() {
+ var keys = [];
+ for (var i = 0; i < stack.length; i++) {
+ keys.push(stack[i].key);
+ }
+ return keys;
+ },
+ top: function() {
+ return stack[stack.length - 1];
+ },
+ remove: function(key) {
+ var idx = -1;
+ for (var i = 0; i < stack.length; i++) {
+ if (key === stack[i].key) {
+ idx = i;
+ break;
+ }
+ }
+ return stack.splice(idx, 1)[0];
+ },
+ removeTop: function() {
+ return stack.pop();
+ },
+ length: function() {
+ return stack.length;
+ }
+ };
+ }
+ };
+ });
\ No newline at end of file
diff --git a/src/modal/test/stackedMap.spec.js b/src/stackedMap/test/stackedMap.spec.js
similarity index 79%
rename from src/modal/test/stackedMap.spec.js
rename to src/stackedMap/test/stackedMap.spec.js
index 47cd24bf1d..62ad5d698e 100644
--- a/src/modal/test/stackedMap.spec.js
+++ b/src/stackedMap/test/stackedMap.spec.js
@@ -1,5 +1,4 @@
-describe('stacked map', function () {
-
+describe('stacked map', function() {
var stackedMap;
beforeEach(module('ui.bootstrap.modal'));
@@ -7,8 +6,7 @@ describe('stacked map', function () {
stackedMap = $$stackedMap.createNew();
}));
- it('should add and remove objects by key', function () {
-
+ 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');
@@ -19,15 +17,14 @@ describe('stacked map', function () {
expect(stackedMap.get('foo')).toBeUndefined();
});
- it('should support listing keys', function () {
+ 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 () {
-
+ it('should get topmost element', function() {
stackedMap.add('foo', 'foo_value');
stackedMap.add('bar', 'bar_value');
expect(stackedMap.length()).toEqual(2);
@@ -36,8 +33,7 @@ describe('stacked map', function () {
expect(stackedMap.length()).toEqual(2);
});
- it('should remove topmost element', function () {
-
+ it('should remove topmost element', function() {
stackedMap.add('foo', 'foo_value');
stackedMap.add('bar', 'bar_value');
@@ -45,13 +41,12 @@ describe('stacked map', function () {
expect(stackedMap.removeTop().key).toEqual('foo');
});
- it('should preserve semantic of an empty stackedMap', function () {
-
+ 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 () {
+ it('should ignore removal of non-existing elements', function() {
expect(stackedMap.remove('non-existing')).toBeUndefined();
});
-});
\ No newline at end of file
+});
diff --git a/src/tabindex/index.js b/src/tabindex/index.js
new file mode 100644
index 0000000000..5d6b5663dc
--- /dev/null
+++ b/src/tabindex/index.js
@@ -0,0 +1,7 @@
+require('./tabindex');
+
+var MODULE_NAME = 'ui.bootstrap.module.tabindex';
+
+angular.module(MODULE_NAME, ['ui.bootstrap.tabindex']);
+
+module.exports = MODULE_NAME;
diff --git a/src/tabindex/tabindex.js b/src/tabindex/tabindex.js
new file mode 100644
index 0000000000..a00a0aff3b
--- /dev/null
+++ b/src/tabindex/tabindex.js
@@ -0,0 +1,12 @@
+angular.module('ui.bootstrap.tabindex', [])
+
+.directive('uibTabindexToggle', function() {
+ return {
+ restrict: 'A',
+ link: function(scope, elem, attrs) {
+ attrs.$observe('disabled', function(disabled) {
+ attrs.$set('tabindex', disabled ? -1 : null);
+ });
+ }
+ };
+});
diff --git a/src/tabindex/test/tabindex.spec.js b/src/tabindex/test/tabindex.spec.js
new file mode 100644
index 0000000000..b1a2159d82
--- /dev/null
+++ b/src/tabindex/test/tabindex.spec.js
@@ -0,0 +1,23 @@
+describe('tabindex toggle directive', function() {
+ var $rootScope, element;
+ beforeEach(module('ui.bootstrap.tabindex'));
+ beforeEach(inject(function($compile, _$rootScope_) {
+ $rootScope = _$rootScope_;
+ element = $compile('foo')($rootScope);
+ $rootScope.$digest();
+ }));
+
+ it('should toggle the tabindex on disabled toggle', function() {
+ expect(element.prop('tabindex')).toBe(0);
+
+ $rootScope.disabled = true;
+ $rootScope.$digest();
+
+ expect(element.prop('tabindex')).toBe(-1);
+
+ $rootScope.disabled = false;
+ $rootScope.$digest();
+
+ expect(element.prop('tabindex')).toBe(0);
+ });
+});
diff --git a/src/tabs/docs/demo.html b/src/tabs/docs/demo.html
index a8e4065487..17b67cef1e 100644
--- a/src/tabs/docs/demo.html
+++ b/src/tabs/docs/demo.html
@@ -1,39 +1,79 @@
+
+
Select a tab by setting active binding to true:
- Select second tab
- Select third tab
+ Select second tab
+ Select third tab
- Enable / Disable third tab
+ Enable / Disable third tab
diff --git a/src/tabs/docs/demo.js b/src/tabs/docs/demo.js
index f3d64fd7cf..d5e1f58b09 100644
--- a/src/tabs/docs/demo.js
+++ b/src/tabs/docs/demo.js
@@ -9,4 +9,8 @@ angular.module('ui.bootstrap.demo').controller('TabsDemoCtrl', function ($scope,
$window.alert('You\'ve selected the alert tab!');
});
};
+
+ $scope.model = {
+ name: 'Tabs'
+ };
});
diff --git a/src/tabs/docs/readme.md b/src/tabs/docs/readme.md
index e43fe2c5c8..169c4dcd7f 100644
--- a/src/tabs/docs/readme.md
+++ b/src/tabs/docs/readme.md
@@ -1,40 +1,64 @@
AngularJS version of the tabs directive.
-### Settings ###
+### uib-tabset settings
-#### `` ####
+* `active`
+
+ _(Default: `Index of first tab`)_ -
+ Active index of tab. Setting this to an existing tab index will make that tab active.
- * `vertical`
- _(Defaults: false)_ :
- Whether tabs appear vertically stacked.
+* `justified`
+ $
+ _(Default: `false`)_ -
+ Whether tabs fill the container and have a consistent width.
- * `justified`
- _(Defaults: false)_ :
- Whether tabs fill the container and have a consistent width.
+ * `template-url`
+ _(Default: `uib/template/tabs/tabset.html`)_ -
+ A URL representing the location of a template to use for the main component.
- * `type`
- _(Defaults: 'tabs')_ :
- Navigation type. Possible values are 'tabs' and 'pills'.
+* `type`
+ _(Defaults: `tabs`)_ -
+ Navigation type. Possible values are 'tabs' and 'pills'.
-#### `` ####
+* `vertical`
+ $
+ _(Default: `false`)_ -
+ Whether tabs appear vertically stacked.
- * `heading` or ``
- :
- Heading text or HTML markup.
+### uib-tab settings
- * `active`
- _(Defaults: false)_ :
- Whether tab is currently selected.
+* `classes`
+ $ -
+ An optional string of space-separated CSS classes.
- * `disable`
- _(Defaults: false)_ :
- Whether tab is clickable and can be activated.
- Note that this was previously the `disabled` attribute, which is now deprecated.
+* `deselect()`
+ $ -
+ An optional expression called when tab is deactivated. Supports `$event` and `$selectedIndex` in template for expression. You may call `$event.preventDefault()` in this event handler to prevent a tab change from occurring. The `$selectedIndex` can be used to determine which tab was attempted to be opened.
- * `select()`
- _(Defaults: null)_ :
- An optional expression called when tab is activated.
-
- * `deselect()`
- _(Defaults: null)_ :
- An optional expression called when tab is deactivated.
+* `disable`
+ $
+
+ _(Default: `false`)_ -
+ Whether tab is clickable and can be activated.
+
+* `heading` -
+ Heading text.
+
+* `index` -
+ Tab index. Must be unique number or string.
+
+* `select()`
+ $ -
+ An optional expression called when tab is activated. Supports $event in template for expression.
+
+* `template-url`
+ _(Default: `uib/template/tabs/tab.html`)_ -
+ A URL representing the location of a template to use for the tab heading.
+
+### Tabset heading
+
+Instead of the `heading` attribute on the `uib-tabset`, you can use an `uib-tab-heading` element inside a tabset that will be used as the tabset's header. There you can use HTML as well.
+
+### Known issues
+
+To use clickable elements within the tab, you have override the tab template to use div elements instead of anchor elements, and replicate the desired styles from Bootstrap's CSS. This is due to browsers interpreting anchor elements as the target of any click event, which triggers routing when certain elements such as buttons are nested inside the anchor element.
diff --git a/src/tabs/index.js b/src/tabs/index.js
new file mode 100644
index 0000000000..b511806126
--- /dev/null
+++ b/src/tabs/index.js
@@ -0,0 +1,9 @@
+require('../../template/tabs/tab.html.js');
+require('../../template/tabs/tabset.html.js');
+require('./tabs');
+
+var MODULE_NAME = 'ui.bootstrap.module.tabs';
+
+angular.module(MODULE_NAME, ['ui.bootstrap.tabs', 'uib/template/tabs/tab.html', 'uib/template/tabs/tabset.html']);
+
+module.exports = MODULE_NAME;
diff --git a/src/tabs/tabs.js b/src/tabs/tabs.js
index 4fbcca06ee..2e8f015c43 100644
--- a/src/tabs/tabs.js
+++ b/src/tabs/tabs.js
@@ -1,197 +1,136 @@
-
-/**
- * @ngdoc overview
- * @name ui.bootstrap.tabs
- *
- * @description
- * AngularJS version of the tabs directive.
- */
-
angular.module('ui.bootstrap.tabs', [])
-.controller('TabsetController', ['$scope', function TabsetCtrl($scope) {
+.controller('UibTabsetController', ['$scope', function ($scope) {
var ctrl = this,
- tabs = ctrl.tabs = $scope.tabs = [];
+ oldIndex;
+ ctrl.tabs = [];
+
+ ctrl.select = function(index, evt) {
+ if (!destroyed) {
+ var previousIndex = findTabIndex(oldIndex);
+ var previousSelected = ctrl.tabs[previousIndex];
+ if (previousSelected) {
+ previousSelected.tab.onDeselect({
+ $event: evt,
+ $selectedIndex: index
+ });
+ if (evt && evt.isDefaultPrevented()) {
+ return;
+ }
+ previousSelected.tab.active = false;
+ }
- ctrl.select = function(selectedTab) {
- angular.forEach(tabs, function(tab) {
- if (tab.active && tab !== selectedTab) {
- tab.active = false;
- tab.onDeselect();
+ var selected = ctrl.tabs[index];
+ if (selected) {
+ selected.tab.onSelect({
+ $event: evt
+ });
+ selected.tab.active = true;
+ ctrl.active = selected.index;
+ oldIndex = selected.index;
+ } else if (!selected && angular.isDefined(oldIndex)) {
+ ctrl.active = null;
+ oldIndex = null;
}
- });
- selectedTab.active = true;
- selectedTab.onSelect();
+ }
};
ctrl.addTab = function addTab(tab) {
- tabs.push(tab);
- // we can't run the select function on the first tab
- // since that would select it twice
- if (tabs.length === 1 && tab.active !== false) {
- tab.active = true;
- } else if (tab.active) {
- ctrl.select(tab);
- }
- else {
- tab.active = false;
+ ctrl.tabs.push({
+ tab: tab,
+ index: tab.index
+ });
+ ctrl.tabs.sort(function(t1, t2) {
+ if (t1.index > t2.index) {
+ return 1;
+ }
+
+ if (t1.index < t2.index) {
+ return -1;
+ }
+
+ return 0;
+ });
+
+ if (tab.index === ctrl.active || !angular.isDefined(ctrl.active) && ctrl.tabs.length === 1) {
+ var newActiveIndex = findTabIndex(tab.index);
+ ctrl.select(newActiveIndex);
}
};
ctrl.removeTab = function removeTab(tab) {
- var index = tabs.indexOf(tab);
- //Select a new tab if the tab to be removed is selected and not destroyed
- if (tab.active && tabs.length > 1 && !destroyed) {
- //If this is the last tab, select the previous tab. else, the next tab.
- var newActiveIndex = index == tabs.length - 1 ? index - 1 : index + 1;
- ctrl.select(tabs[newActiveIndex]);
+ var index;
+ for (var i = 0; i < ctrl.tabs.length; i++) {
+ if (ctrl.tabs[i].tab === tab) {
+ index = i;
+ break;
+ }
}
- tabs.splice(index, 1);
+
+ if (ctrl.tabs[index].index === ctrl.active) {
+ var newActiveTabIndex = index === ctrl.tabs.length - 1 ?
+ index - 1 : index + 1 % ctrl.tabs.length;
+ ctrl.select(newActiveTabIndex);
+ }
+
+ ctrl.tabs.splice(index, 1);
};
+ $scope.$watch('tabset.active', function(val) {
+ if (angular.isDefined(val) && val !== oldIndex) {
+ ctrl.select(findTabIndex(val));
+ }
+ });
+
var destroyed;
$scope.$on('$destroy', function() {
destroyed = true;
});
+
+ function findTabIndex(index) {
+ for (var i = 0; i < ctrl.tabs.length; i++) {
+ if (ctrl.tabs[i].index === index) {
+ return i;
+ }
+ }
+ }
}])
-/**
- * @ngdoc directive
- * @name ui.bootstrap.tabs.directive:tabset
- * @restrict EA
- *
- * @description
- * Tabset is the outer container for the tabs directive
- *
- * @param {boolean=} vertical Whether or not to use vertical styling for the tabs.
- * @param {boolean=} justified Whether or not to use justified styling for the tabs.
- *
- * @example
-
-
-
- First Content!
- Second Content!
-
-
-
- First Vertical Content!
- Second Vertical Content!
-
-
- First Justified Content!
- Second Justified Content!
-
-
-
- */
-.directive('tabset', function() {
+.directive('uibTabset', function() {
return {
- restrict: 'EA',
transclude: true,
replace: true,
- scope: {
+ scope: {},
+ bindToController: {
+ active: '=?',
type: '@'
},
- controller: 'TabsetController',
- templateUrl: 'template/tabs/tabset.html',
+ controller: 'UibTabsetController',
+ controllerAs: 'tabset',
+ templateUrl: function(element, attrs) {
+ return attrs.templateUrl || 'uib/template/tabs/tabset.html';
+ },
link: function(scope, element, attrs) {
- scope.vertical = angular.isDefined(attrs.vertical) ? scope.$parent.$eval(attrs.vertical) : false;
- scope.justified = angular.isDefined(attrs.justified) ? scope.$parent.$eval(attrs.justified) : false;
+ scope.vertical = angular.isDefined(attrs.vertical) ?
+ scope.$parent.$eval(attrs.vertical) : false;
+ scope.justified = angular.isDefined(attrs.justified) ?
+ scope.$parent.$eval(attrs.justified) : false;
}
};
})
-/**
- * @ngdoc directive
- * @name ui.bootstrap.tabs.directive:tab
- * @restrict EA
- *
- * @param {string=} heading The visible heading, or title, of the tab. Set HTML headings with {@link ui.bootstrap.tabs.directive:tabHeading tabHeading}.
- * @param {string=} select An expression to evaluate when the tab is selected.
- * @param {boolean=} active A binding, telling whether or not this tab is selected.
- * @param {boolean=} disabled A binding, telling whether or not this tab is disabled.
- *
- * @description
- * Creates a tab with a heading and content. Must be placed within a {@link ui.bootstrap.tabs.directive:tabset tabset}.
- *
- * @example
-
-
-
-
- Select item 1, using active binding
-
-
- Enable/disable item 2, using disabled binding
-
-
-
- First Tab
-
- Alert me!
- Second Tab, with alert callback and html heading!
-
-
- {{item.content}}
-
-
-
-
-
- function TabsDemoCtrl($scope) {
- $scope.items = [
- { title:"Dynamic Title 1", content:"Dynamic Item 0" },
- { title:"Dynamic Title 2", content:"Dynamic Item 1", disabled: true }
- ];
-
- $scope.alertMe = function() {
- setTimeout(function() {
- alert("You've selected the alert tab!");
- });
- };
- };
-
-
- */
-
-/**
- * @ngdoc directive
- * @name ui.bootstrap.tabs.directive:tabHeading
- * @restrict EA
- *
- * @description
- * Creates an HTML heading for a {@link ui.bootstrap.tabs.directive:tab tab}. Must be placed as a child of a tab element.
- *
- * @example
-
-
-
-
- HTML in my titles?!
- And some content, too!
-
-
- Icon heading?!?
- That's right.
-
-
-
-
- */
-.directive('tab', ['$parse', '$log', function($parse, $log) {
+.directive('uibTab', ['$parse', function($parse) {
return {
- require: '^tabset',
- restrict: 'EA',
+ require: '^uibTabset',
replace: true,
- templateUrl: 'template/tabs/tab.html',
+ templateUrl: function(element, attrs) {
+ return attrs.templateUrl || 'uib/template/tabs/tab.html';
+ },
transclude: true,
scope: {
- active: '=?',
heading: '@',
+ index: '=?',
+ classes: '@?',
onSelect: '&select', //This callback is called in contentHeadingTransclude
//once it inserts the tab's content into the dom
onDeselect: '&deselect'
@@ -199,56 +138,58 @@ angular.module('ui.bootstrap.tabs', [])
controller: function() {
//Empty controller so other directives can require being 'under' a tab
},
- compile: function(elm, attrs, transclude) {
- return function postLink(scope, elm, attrs, tabsetCtrl) {
- scope.$watch('active', function(active) {
- if (active) {
- tabsetCtrl.select(scope);
- }
+ controllerAs: 'tab',
+ link: function(scope, elm, attrs, tabsetCtrl, transclude) {
+ scope.disabled = false;
+ if (attrs.disable) {
+ scope.$parent.$watch($parse(attrs.disable), function(value) {
+ scope.disabled = !! value;
});
+ }
- scope.disabled = false;
- if ( attrs.disable ) {
- scope.$parent.$watch($parse(attrs.disable), function(value) {
- scope.disabled = !! value;
- });
+ if (angular.isUndefined(attrs.index)) {
+ if (tabsetCtrl.tabs && tabsetCtrl.tabs.length) {
+ scope.index = Math.max.apply(null, tabsetCtrl.tabs.map(function(t) { return t.index; })) + 1;
+ } else {
+ scope.index = 0;
}
+ }
- // Deprecation support of "disabled" parameter
- // fix(tab): IE9 disabled attr renders grey text on enabled tab #2677
- // This code is duplicated from the lines above to make it easy to remove once
- // the feature has been completely deprecated
- if ( attrs.disabled ) {
- $log.warn('Use of "disabled" attribute has been deprecated, please use "disable"');
- scope.$parent.$watch($parse(attrs.disabled), function(value) {
- scope.disabled = !! value;
- });
- }
+ if (angular.isUndefined(attrs.classes)) {
+ scope.classes = '';
+ }
- scope.select = function() {
- if ( !scope.disabled ) {
- scope.active = true;
+ scope.select = function(evt) {
+ if (!scope.disabled) {
+ var index;
+ for (var i = 0; i < tabsetCtrl.tabs.length; i++) {
+ if (tabsetCtrl.tabs[i].tab === scope) {
+ index = i;
+ break;
+ }
}
- };
-
- tabsetCtrl.addTab(scope);
- scope.$on('$destroy', function() {
- tabsetCtrl.removeTab(scope);
- });
- //We need to transclude later, once the content container is ready.
- //when this link happens, we're inside a tab heading.
- scope.$transcludeFn = transclude;
+ tabsetCtrl.select(index, evt);
+ }
};
+
+ tabsetCtrl.addTab(scope);
+ scope.$on('$destroy', function() {
+ tabsetCtrl.removeTab(scope);
+ });
+
+ //We need to transclude later, once the content container is ready.
+ //when this link happens, we're inside a tab heading.
+ scope.$transcludeFn = transclude;
}
};
}])
-.directive('tabHeadingTransclude', [function() {
+.directive('uibTabHeadingTransclude', function() {
return {
restrict: 'A',
- require: '^tab',
- link: function(scope, elm, attrs, tabCtrl) {
+ require: '^uibTab',
+ link: function(scope, elm) {
scope.$watch('headingElement', function updateHeadingElement(heading) {
if (heading) {
elm.html('');
@@ -257,14 +198,14 @@ angular.module('ui.bootstrap.tabs', [])
});
}
};
-}])
+})
-.directive('tabContentTransclude', function() {
+.directive('uibTabContentTransclude', function() {
return {
restrict: 'A',
- require: '^tabset',
+ require: '^uibTabset',
link: function(scope, elm, attrs) {
- var tab = scope.$eval(attrs.tabContentTransclude);
+ var tab = scope.$eval(attrs.uibTabContentTransclude).tab;
//Now our tab is ready to be transcluded: both the tab heading area
//and the tab content area are loaded. Transclude 'em both.
@@ -280,14 +221,16 @@ angular.module('ui.bootstrap.tabs', [])
});
}
};
+
function isTabHeading(node) {
- return node.tagName && (
- node.hasAttribute('tab-heading') ||
- node.hasAttribute('data-tab-heading') ||
- node.tagName.toLowerCase() === 'tab-heading' ||
- node.tagName.toLowerCase() === 'data-tab-heading'
+ return node.tagName && (
+ node.hasAttribute('uib-tab-heading') ||
+ node.hasAttribute('data-uib-tab-heading') ||
+ node.hasAttribute('x-uib-tab-heading') ||
+ node.tagName.toLowerCase() === 'uib-tab-heading' ||
+ node.tagName.toLowerCase() === 'data-uib-tab-heading' ||
+ node.tagName.toLowerCase() === 'x-uib-tab-heading' ||
+ node.tagName.toLowerCase() === 'uib:tab-heading'
);
}
-})
-
-;
+});
diff --git a/src/tabs/test/tabs.spec.js b/src/tabs/test/tabs.spec.js
index 8d91ecbee7..76bfd7c6db 100644
--- a/src/tabs/test/tabs.spec.js
+++ b/src/tabs/test/tabs.spec.js
@@ -1,7 +1,10 @@
describe('tabs', function() {
- beforeEach(module('ui.bootstrap.tabs', 'template/tabs/tabset.html', 'template/tabs/tab.html'));
-
var elm, scope;
+
+ beforeEach(module('ui.bootstrap.tabs'));
+ beforeEach(module('uib/template/tabs/tabset.html'));
+ beforeEach(module('uib/template/tabs/tab.html'));
+
function titles() {
return elm.find('ul.nav-tabs li');
}
@@ -12,40 +15,49 @@ describe('tabs', function() {
function expectTitles(titlesArray) {
var t = titles();
expect(t.length).toEqual(titlesArray.length);
- for (var i=0; i',
- ' ',
+ '',
+ ' ',
' first content is {{first}}',
- ' ',
- ' ',
- ' Second Tab {{second}}',
+ ' ',
+ ' ',
+ ' Second Tab {{second}}',
' second content is {{second}}',
- ' ',
- ''
+ ' ',
+ ' ',
+ ' Second Tab {{third}}',
+ ' third content is {{third}}',
+ ' ',
+ ''
].join('\n'))(scope);
scope.$apply();
return elm;
@@ -54,75 +66,105 @@ describe('tabs', function() {
it('should pass class and other attributes on to tab template', function() {
expect(elm).toHaveClass('hello');
expect(elm.attr('data-pizza')).toBe('pepperoni');
+ //Ensure that we have bootstrap 4 link class so things are future proofed.
+ var link = $(elm.find('a')[0]);
+ expect(link).toHaveClass('nav-link');
});
it('should create clickable titles', function() {
var t = titles();
- expect(t.length).toBe(2);
- expect(t.find('a').eq(0).text()).toBe('First Tab 1');
- //It should put the tab-heading element into the 'a' title
- expect(t.find('a').eq(1).children().is('tab-heading')).toBe(true);
- expect(t.find('a').eq(1).children().html()).toBe('Second Tab 2');
+ expect(t.length).toBe(3);
+ expect(t.find('> a').eq(0).text()).toBe('First Tab 1');
+ //It should put the uib-tab-heading element into the 'a' title
+ expect(t.find('> a').eq(1).children().is('uib-tab-heading')).toBe(true);
+ expect(t.find('> a').eq(1).children().html()).toBe('Second Tab 2');
});
it('should bind tabs content and set first tab active', function() {
- expectContents(['first content is 1', 'second content is 2']);
+ expectContents(['first content is 1', 'second content is 2', 'third content is 3']);
expect(titles().eq(0)).toHaveClass('active');
expect(titles().eq(1)).not.toHaveClass('active');
- expect(scope.actives.one).toBe(true);
- expect(scope.actives.two).toBeFalsy();
+ expect(scope.active).toBe(1);
+ });
+
+ it('should set optional classes on each tab', function() {
+ expect(titles().eq(0)).toHaveClass(scope.firstClass);
+
+ var secondClassArr = scope.secondClass.split(' ');
+ secondClassArr.forEach(function(clazz) {
+ expect(titles().eq(1)).toHaveClass(clazz);
+ });
});
it('should change active on click', function() {
- titles().eq(1).find('a').click();
+ titles().eq(1).find('> a').click();
expect(contents().eq(1)).toHaveClass('active');
expect(titles().eq(0)).not.toHaveClass('active');
expect(titles().eq(1)).toHaveClass('active');
- expect(scope.actives.one).toBe(false);
- expect(scope.actives.two).toBe(true);
+ expect(scope.active).toBe(2);
});
it('should call select callback on select', function() {
- titles().eq(1).find('a').click();
+ expect(scope.selectFirst.calls.count()).toBe(1);
+ titles().eq(1).find('> a').click();
expect(scope.selectSecond).toHaveBeenCalled();
- titles().eq(0).find('a').click();
+ expect(scope.selectSecond.calls.argsFor(0)[0].target).toBe(titles().eq(1).find('> a')[0]);
+ titles().eq(0).find('> a').click();
expect(scope.selectFirst).toHaveBeenCalled();
+ expect(scope.selectFirst.calls.argsFor(1)[0].target).toBe(titles().eq(0).find('> a')[0]);
});
it('should call deselect callback on deselect', function() {
- titles().eq(1).find('a').click();
- titles().eq(0).find('a').click();
- expect(scope.deselectSecond).toHaveBeenCalled();
- titles().eq(1).find('a').click();
+ titles().eq(1).find('> a').click();
expect(scope.deselectFirst).toHaveBeenCalled();
+ expect(scope.deselectFirst.calls.argsFor(0)[0].target).toBe(titles().eq(1).find('> a')[0]);
+ expect(scope.deselectFirst.calls.argsFor(0)[1]).toBe(1);
+ titles().eq(0).find('> a').click();
+ expect(scope.deselectSecond).toHaveBeenCalled();
+ expect(scope.deselectSecond.calls.argsFor(0)[0].target).toBe(titles().eq(0).find('> a')[0]);
+ expect(scope.deselectSecond.calls.argsFor(0)[1]).toBe(0);
+ titles().eq(1).find('> a').click();
+ expect(scope.deselectFirst.calls.count()).toBe(2);
+ expect(scope.deselectFirst.calls.argsFor(1)[0].target).toBe(titles().eq(1).find('> a')[0]);
+ expect(scope.deselectFirst.calls.argsFor(1)[1]).toBe(1);
+ });
+
+ it('should prevent tab deselection when $event.preventDefault() is called', function() {
+ spyOn(scope, 'deselectThird');
+ titles().eq(2).find('> a').click();
+ expect(scope.active).toBe(3);
+ titles().eq(1).find('> a').click();
+ expect(scope.deselectThird).toHaveBeenCalled();
+ expect(scope.active).not.toBe(1);
+ expect(scope.active).toBe(2);
});
});
describe('basics with initial active tab', function() {
-
beforeEach(inject(function($compile, $rootScope) {
scope = $rootScope.$new();
- function makeTab(active) {
+ function makeTab(index) {
return {
- active: !!active,
+ index: index,
select: jasmine.createSpy()
};
}
scope.tabs = [
- makeTab(), makeTab(), makeTab(true), makeTab()
+ makeTab(1), makeTab(3), makeTab(5), makeTab(7)
];
+ scope.active = 5;
elm = $compile([
- '',
- ' ',
- ' ',
- ' ',
- ' ',
- ' ',
- ' ',
- ' ',
- ' ',
- ''
+ '',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ''
].join('\n'))(scope);
scope.$apply();
}));
@@ -131,13 +173,13 @@ describe('tabs', function() {
var _titles = titles();
angular.forEach(scope.tabs, function(tab, i) {
if (activeTab === tab) {
- expect(tab.active).toBe(true);
+ expect(scope.active).toBe(tab.index);
//It should only call select ONCE for each select
expect(tab.select).toHaveBeenCalled();
expect(_titles.eq(i)).toHaveClass('active');
expect(contents().eq(i)).toHaveClass('active');
} else {
- expect(tab.active).toBe(false);
+ expect(scope.active).not.toBe(tab.index);
expect(_titles.eq(i)).not.toHaveClass('active');
}
});
@@ -149,12 +191,81 @@ describe('tabs', function() {
});
});
+ describe('without active binding and index attributes', function() {
+ beforeEach(inject(function($compile, $rootScope) {
+ scope = $rootScope.$new();
+ scope.first = '1';
+ scope.second = '2';
+ elm = $compile([
+ '',
+ ' ',
+ ' first content is {{first}}',
+ ' ',
+ ' ',
+ ' second content is {{second}}',
+ ' ',
+ ''
+ ].join('\n'))(scope);
+ scope.$apply();
+ return elm;
+ }));
+
+ it('should bind tabs content and set first tab active', function() {
+ expectContents(['first content is 1', 'second content is 2']);
+ expect(titles().eq(0)).toHaveClass('active');
+ expect(titles().eq(1)).not.toHaveClass('active');
+ expect(elm.controller('uibTabset').active).toBe(0);
+ });
+
+ it('should change active on click', function() {
+ titles().eq(1).find('> a').click();
+ expect(contents().eq(1)).toHaveClass('active');
+ expect(titles().eq(0)).not.toHaveClass('active');
+ expect(titles().eq(1)).toHaveClass('active');
+ expect(elm.controller('uibTabset').active).toBe(1);
+ });
+ });
+
+ describe('index as strings', function() {
+ beforeEach(inject(function($compile, $rootScope) {
+ scope = $rootScope.$new();
+ scope.first = 'one';
+ scope.second = 'two';
+ scope.active = 'two';
+ elm = $compile([
+ '',
+ ' ',
+ ' first content',
+ ' ',
+ ' ',
+ ' second content',
+ ' ',
+ ''
+ ].join('\n'))(scope);
+ scope.$apply();
+ return elm;
+ }));
+
+ it('should set second tab active', function() {
+ expect(titles().eq(0)).not.toHaveClass('active');
+ expect(titles().eq(1)).toHaveClass('active');
+ expect(elm.controller('uibTabset').active).toBe('two');
+ });
+
+ it('should change active on click', function() {
+ expect(titles().eq(0)).not.toHaveClass('active');
+ titles().eq(0).find('> a').click();
+ expect(titles().eq(0)).toHaveClass('active');
+ expect(titles().eq(1)).not.toHaveClass('active');
+ expect(elm.controller('uibTabset').active).toBe('one');
+ });
+ });
+
describe('tab callback order', function() {
var execOrder;
beforeEach(inject(function($compile, $rootScope) {
scope = $rootScope.$new();
execOrder = [];
- scope.actives = {};
scope.execute = function(id) {
execOrder.push(id);
@@ -162,10 +273,10 @@ describe('tabs', function() {
elm = $compile([
'
- Pellentesque {{dynamicTooltipText}},
+ Pellentesque {{dynamicTooltipText}},
sit amet venenatis urna cursus eget nunc scelerisque viverra mauris, in
aliquam. Tincidunt lobortis feugiat vivamus at
- left eget
- arcu dictum varius duis at consectetur lorem. Vitae elementum curabitur
- right
+ fading
+ eget arcu dictum varius duis at consectetur lorem. Vitae elementum curabitur
+ show delay
nunc sed velit dignissim sodales ut eu sem integer vitae. Turpis egestas
- bottom
+ hide delay
pharetra convallis posuere morbi leo urna,
- fading
- at elementum eu, facilisis sed odio morbi quis commodo odio. In cursus
- delayed turpis massa tincidunt dui ut.
- Custom template
- nunc sed velit dignissim sodales ut eu sem integer vitae. Turpis egestas
+ Custom template
+ at elementum eu, facilisis sed odio morbi quis commodo odio.
diff --git a/src/tooltip/docs/demo.js b/src/tooltip/docs/demo.js
index 18c2794b3e..d4a2cc02a8 100644
--- a/src/tooltip/docs/demo.js
+++ b/src/tooltip/docs/demo.js
@@ -2,4 +2,21 @@ angular.module('ui.bootstrap.demo').controller('TooltipDemoCtrl', function ($sco
$scope.dynamicTooltip = 'Hello, World!';
$scope.dynamicTooltipText = 'dynamic';
$scope.htmlTooltip = $sce.trustAsHtml('I\'ve been made bold!');
+ $scope.placement = {
+ options: [
+ 'top',
+ 'top-left',
+ 'top-right',
+ 'bottom',
+ 'bottom-left',
+ 'bottom-right',
+ 'left',
+ 'left-top',
+ 'left-bottom',
+ 'right',
+ 'right-top',
+ 'right-bottom'
+ ],
+ selected: 'top'
+ };
});
diff --git a/src/tooltip/docs/readme.md b/src/tooltip/docs/readme.md
index 78902756e3..1bc4b04d44 100644
--- a/src/tooltip/docs/readme.md
+++ b/src/tooltip/docs/readme.md
@@ -1,63 +1,122 @@
A lightweight, extensible directive for fancy tooltip creation. The tooltip
directive supports multiple placements, optional transition animation, and more.
-There are three versions of the tooltip: `tooltip`, `tooltip-template`, and
-`tooltip-html-unsafe`:
-
-- `tooltip` takes text only and will escape any HTML provided.
-- `tooltip-template` takes text that specifies the location of a template to
- use for the tooltip.
-- `tooltip-html` takes
- whatever HTML is provided and displays it in a tooltip; *The user is responsible for ensuring the
- content is safe to put into the DOM!*
-- `tooltip-html-unsafe` -- deprecated in favour of `tooltip-html`
-
-The tooltip directives provide several optional attributes to control how they
-will display:
-
-- `tooltip-placement`: Where to place it? Defaults to "top", but also accepts
- "bottom", "left", "right".
-- `tooltip-animation`: Should it fade in and out? Defaults to "true".
-- `tooltip-popup-delay`: For how long should the user have to have the mouse
- over the element before the tooltip shows (in milliseconds)? Defaults to 0.
-- `tooltip-trigger`: What should trigger a show of the tooltip?
- Note: this attribute is no longer observable. See `tooltip-enable`.
-- `tooltip-enable`: Is it enabled? It will enable or disable the configured
- `tooltip-trigger`.
-- `tooltip-append-to-body`: Should the tooltip be appended to `$body` instead of
- the parent element?
-- `tooltip-class`: Custom class to be applied to the tooltip.
-
-The tooltip directives require the `$position` service.
-
-**Triggers**
-
-The following show triggers are supported out of the box, along with their
-provided hide triggers:
+__Note to mobile developers__: Please note that while tooltips may work correctly on mobile devices (including tablets),
+ we have made the decision to not officially support such a use-case because it does not make sense from a UX perspective.
+
+There are three versions of the tooltip: `uib-tooltip`, `uib-tooltip-template`, and
+`uib-tooltip-html`:
+
+* `uib-tooltip` -
+ Takes text only and will escape any HTML provided.
+* `uib-tooltip-html`
+ $ -
+ Takes an expression that evaluates to an HTML string. Note that this HTML is not compiled. If compilation is required, please use the `uib-tooltip-template` attribute option instead. *The user is responsible for ensuring the content is safe to put into the DOM!*
+* `uib-tooltip-template`
+ $ -
+ Takes text that specifies the location of a template to use for the tooltip. Note that this needs to be wrapped in a tag.
+
+### uib-tooltip-* settings
+
+All these settings are available for the three types of tooltips.
+
+* `tooltip-animation`
+ $
+ C
+ _(Default: `true`, Config: `animation`)_ -
+ Should it fade in and out?
+
+* `tooltip-append-to-body`
+ $
+ C
+ _(Default: `false`, Config: `appendToBody`)_ -
+ Should the tooltip be appended to '$body' instead of the parent element?
+
+* `tooltip-class` -
+ Custom class to be applied to the tooltip.
+
+* `tooltip-enable`
+ $
+ _(Default: `true`)_ -
+ Is it enabled? It will enable or disable the configured tooltip-trigger.
+
+* `tooltip-is-open`
+
+ _(Default: `false`)_ -
+ Whether to show the tooltip.
+
+* `tooltip-placement`
+ C
+ _(Default: `top`, Config: `placement`)_ -
+ Passing in 'auto' separated by a space before the placement will enable auto positioning, e.g: "auto bottom-left". The tooltip will attempt to position where it fits in the closest scrollable ancestor. Accepts:
+
+ * `top` - tooltip on top, horizontally centered on host element.
+ * `top-left` - tooltip on top, left edge aligned with host element left edge.
+ * `top-right` - tooltip on top, right edge aligned with host element right edge.
+ * `bottom` - tooltip on bottom, horizontally centered on host element.
+ * `bottom-left` - tooltip on bottom, left edge aligned with host element left edge.
+ * `bottom-right` - tooltip on bottom, right edge aligned with host element right edge.
+ * `left` - tooltip on left, vertically centered on host element.
+ * `left-top` - tooltip on left, top edge aligned with host element top edge.
+ * `left-bottom` - tooltip on left, bottom edge aligned with host element bottom edge.
+ * `right` - tooltip on right, vertically centered on host element.
+ * `right-top` - tooltip on right, top edge aligned with host element top edge.
+ * `right-bottom` - tooltip on right, bottom edge aligned with host element bottom edge.
+
+* `tooltip-popup-close-delay`
+ C
+ _(Default: `0`, Config: `popupCloseDelay`)_ -
+ For how long should the tooltip remain open after the close trigger event?
+
+* `tooltip-popup-delay`
+ C
+ _(Default: `0`, Config: `popupDelay`)_ -
+ Popup delay in milliseconds until it opens.
+
+* `tooltip-trigger`
+ $
+ _(Default: `'mouseenter'`)_ -
+ What should trigger a show of the tooltip? Supports a space separated list of event names, or objects (see below).
+
+**Note:** To configure the tooltips, you need to do it on `$uibTooltipProvider` (also see below).
+
+### Triggers
+
+The following show triggers are supported out of the box, along with their provided hide triggers:
- `mouseenter`: `mouseleave`
- `click`: `click`
+- `outsideClick`: `outsideClick`
- `focus`: `blur`
+- `none`
+
+The `outsideClick` trigger will cause the tooltip to toggle on click, and hide when anything else is clicked.
For any non-supported value, the trigger will be used to both show and hide the
-tooltip.
+tooltip. Using the 'none' trigger will disable the internal trigger(s), one can
+then use the `tooltip-is-open` attribute exclusively to show and hide the tooltip.
-**$tooltipProvider**
+### $uibTooltipProvider
-Through the `$tooltipProvider`, you can change the way tooltips and popovers
+Through the `$uibTooltipProvider`, you can change the way tooltips and popovers
behave by default; the attributes above always take precedence. The following
methods are available:
-- `setTriggers( obj )`: Extends the default trigger mappings mentioned above
- with mappings of your own. E.g. `{ 'openTrigger': 'closeTrigger' }`.
-- `options( obj )`: Provide a set of defaults for certain tooltip and popover
- attributes. Currently supports 'placement', 'animation', 'popupDelay', and
- `appendToBody`. Here are the defaults:
+* `setTriggers(obj)`
+ _(Example: `{ 'openTrigger': 'closeTrigger' }`)_ -
+ Extends the default trigger mappings mentioned above with mappings of your own.
+
+* `options(obj)` -
+ Provide a set of defaults for certain tooltip and popover attributes. Currently supports the ones with the C badge.
+
+### Known issues
+
+For Safari 7+ support, if you want to use the **focus** `tooltip-trigger`, you need to use an anchor tag with a tab index. For example:
-
+```
+
+ Click Me
+
+```
+For Safari (potentially all versions up to 9), there is an issue with the hover CSS selector when using multiple elements grouped close to each other that are using the tooltip - it is possible for multiple elements to gain the hover state when mousing between the elements quickly and exiting the container at the right time. See [issue #5445](https://github.com/angular-ui/bootstrap/issues/5445) for more details.
diff --git a/src/tooltip/index-nocss.js b/src/tooltip/index-nocss.js
new file mode 100644
index 0000000000..627ad2e77f
--- /dev/null
+++ b/src/tooltip/index-nocss.js
@@ -0,0 +1,12 @@
+require('../position/index-nocss.js');
+require('../stackedMap');
+require('../../template/tooltip/tooltip-popup.html.js');
+require('../../template/tooltip/tooltip-html-popup.html.js');
+require('../../template/tooltip/tooltip-template-popup.html.js');
+require('./tooltip');
+
+var MODULE_NAME = 'ui.bootstrap.module.tooltip';
+
+angular.module(MODULE_NAME, ['ui.bootstrap.tooltip', 'uib/template/tooltip/tooltip-popup.html', 'uib/template/tooltip/tooltip-html-popup.html', 'uib/template/tooltip/tooltip-template-popup.html']);
+
+module.exports = MODULE_NAME;
diff --git a/src/tooltip/index.js b/src/tooltip/index.js
new file mode 100644
index 0000000000..21d563381f
--- /dev/null
+++ b/src/tooltip/index.js
@@ -0,0 +1,3 @@
+require('../position/position.css');
+require('./tooltip.css');
+module.exports = require('./index-nocss.js');
diff --git a/src/tooltip/test/tooltip-template.spec.js b/src/tooltip/test/tooltip-template.spec.js
index 190be308c7..e474c6dbc3 100644
--- a/src/tooltip/test/tooltip-template.spec.js
+++ b/src/tooltip/test/tooltip-template.spec.js
@@ -3,21 +3,23 @@ describe('tooltip template', function() {
elmBody,
scope,
elmScope,
- tooltipScope;
+ tooltipScope,
+ $document;
// load the popover code
beforeEach(module('ui.bootstrap.tooltip'));
// load the template
- beforeEach(module('template/tooltip/tooltip-template-popup.html'));
+ beforeEach(module('uib/template/tooltip/tooltip-template-popup.html'));
- beforeEach(inject(function ($templateCache) {
+ beforeEach(inject(function($templateCache) {
$templateCache.put('myUrl', [200, '{{ myTemplateText }}', {}]);
}));
- beforeEach(inject(function($rootScope, $compile) {
+ beforeEach(inject(function($rootScope, $compile, _$document_) {
+ $document = _$document_;
elmBody = angular.element(
- '
';
return {
- restrict: 'EA',
- compile: function (tElem, tAttrs) {
- var tooltipLinker = $compile( template );
+ compile: function(tElem, tAttrs) {
+ var tooltipLinker = $compile(template);
- return function link ( scope, element, attrs, tooltipCtrl ) {
+ return function link(scope, element, attrs, tooltipCtrl) {
var tooltip;
var tooltipLinkedScope;
var transitionTimeout;
- var popupTimeout;
- var appendToBody = angular.isDefined( options.appendToBody ) ? options.appendToBody : false;
- var triggers = getTriggers( undefined );
- var hasEnableExp = angular.isDefined(attrs[prefix+'Enable']);
+ var showTimeout;
+ var hideTimeout;
+ var positionTimeout;
+ var adjustmentTimeout;
+ var appendToBody = angular.isDefined(options.appendToBody) ? options.appendToBody : false;
+ var triggers = getTriggers(undefined);
+ var hasEnableExp = angular.isDefined(attrs[prefix + 'Enable']);
var ttScope = scope.$new(true);
+ var repositionScheduled = false;
+ var isOpenParse = angular.isDefined(attrs[prefix + 'IsOpen']) ? $parse(attrs[prefix + 'IsOpen']) : false;
+ var contentParse = options.useContentExp ? $parse(attrs[ttType]) : false;
+ var observers = [];
+ var lastPlacement;
+
+ var positionTooltip = function() {
+ // check if tooltip exists and is not empty
+ if (!tooltip || !tooltip.html()) { return; }
+
+ if (!positionTimeout) {
+ positionTimeout = $timeout(function() {
+ var ttPosition = $position.positionElements(element, tooltip, ttScope.placement, appendToBody);
+ var initialHeight = angular.isDefined(tooltip.offsetHeight) ? tooltip.offsetHeight : tooltip.prop('offsetHeight');
+ var elementPos = appendToBody ? $position.offset(element) : $position.position(element);
+ tooltip.css({ top: ttPosition.top + 'px', left: ttPosition.left + 'px' });
+ var placementClasses = ttPosition.placement.split('-');
+
+ if (!tooltip.hasClass(placementClasses[0])) {
+ tooltip.removeClass(lastPlacement.split('-')[0]);
+ tooltip.addClass(placementClasses[0]);
+ }
- var positionTooltip = function () {
- if (!tooltip) { return; }
+ if (!tooltip.hasClass(options.placementClassPrefix + ttPosition.placement)) {
+ tooltip.removeClass(options.placementClassPrefix + lastPlacement);
+ tooltip.addClass(options.placementClassPrefix + ttPosition.placement);
+ }
- var ttPosition = $position.positionElements(element, tooltip, ttScope.placement, appendToBody);
- ttPosition.top += 'px';
- ttPosition.left += 'px';
+ adjustmentTimeout = $timeout(function() {
+ var currentHeight = angular.isDefined(tooltip.offsetHeight) ? tooltip.offsetHeight : tooltip.prop('offsetHeight');
+ var adjustment = $position.adjustTop(placementClasses, elementPos, initialHeight, currentHeight);
+ if (adjustment) {
+ tooltip.css(adjustment);
+ }
+ adjustmentTimeout = null;
+ }, 0, false);
+
+ // first time through tt element will have the
+ // uib-position-measure class or if the placement
+ // has changed we need to position the arrow.
+ if (tooltip.hasClass('uib-position-measure')) {
+ $position.positionArrow(tooltip, ttPosition.placement);
+ tooltip.removeClass('uib-position-measure');
+ } else if (lastPlacement !== ttPosition.placement) {
+ $position.positionArrow(tooltip, ttPosition.placement);
+ }
+ lastPlacement = ttPosition.placement;
- // Now set the calculated positioning.
- tooltip.css( ttPosition );
+ positionTimeout = null;
+ }, 0, false);
+ }
};
// Set up the correct scope to allow transclusion later
@@ -143,8 +209,8 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap
// TODO add ability to start tooltip opened
ttScope.isOpen = false;
- function toggleTooltipBind () {
- if ( ! ttScope.isOpen ) {
+ function toggleTooltipBind() {
+ if (!ttScope.isOpen) {
showTooltipBind();
} else {
hideTooltipBind();
@@ -153,213 +219,360 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap
// Show the tooltip with delay if specified, otherwise show it immediately
function showTooltipBind() {
- if(hasEnableExp && !scope.$eval(attrs[prefix+'Enable'])) {
+ if (hasEnableExp && !scope.$eval(attrs[prefix + 'Enable'])) {
return;
}
+ cancelHide();
prepareTooltip();
- if ( ttScope.popupDelay ) {
+ if (ttScope.popupDelay) {
// Do nothing if the tooltip was already scheduled to pop-up.
// This happens if show is triggered multiple times before any hide is triggered.
- if (!popupTimeout) {
- popupTimeout = $timeout( show, ttScope.popupDelay, false );
- popupTimeout.then(function(reposition){reposition();});
+ if (!showTimeout) {
+ showTimeout = $timeout(show, ttScope.popupDelay, false);
}
} else {
- show()();
+ show();
}
}
- function hideTooltipBind () {
- scope.$apply(function () {
+ function hideTooltipBind() {
+ cancelShow();
+
+ if (ttScope.popupCloseDelay) {
+ if (!hideTimeout) {
+ hideTimeout = $timeout(hide, ttScope.popupCloseDelay, false);
+ }
+ } else {
hide();
- });
+ }
}
// Show the tooltip popup element.
function show() {
-
- popupTimeout = null;
-
- // If there is a pending remove transition, we must cancel it, lest the
- // tooltip be mysteriously removed.
- if ( transitionTimeout ) {
- $timeout.cancel( transitionTimeout );
- transitionTimeout = null;
- }
+ cancelShow();
+ cancelHide();
// Don't show empty tooltips.
- if ( !(options.useContentExp ? ttScope.contentExp() : ttScope.content) ) {
+ if (!ttScope.content) {
return angular.noop;
}
createTooltip();
- // Set the initial positioning.
- tooltip.css({ top: 0, left: 0, display: 'block' });
- ttScope.$digest();
-
- positionTooltip();
-
// And show the tooltip.
- ttScope.isOpen = true;
- ttScope.$apply(); // digest required as $apply is not called
+ ttScope.$evalAsync(function() {
+ ttScope.isOpen = true;
+ assignIsOpen(true);
+ positionTooltip();
+ });
+ }
- // Return positioning function as promise callback for correct
- // positioning after draw.
- return positionTooltip;
+ function cancelShow() {
+ if (showTimeout) {
+ $timeout.cancel(showTimeout);
+ showTimeout = null;
+ }
+
+ if (positionTimeout) {
+ $timeout.cancel(positionTimeout);
+ positionTimeout = null;
+ }
}
// Hide the tooltip popup element.
function hide() {
+ if (!ttScope) {
+ return;
+ }
+
// First things first: we don't show it anymore.
- ttScope.isOpen = false;
-
- //if tooltip is going to be shown after delay, we must cancel this
- $timeout.cancel( popupTimeout );
- popupTimeout = null;
-
- // And now we remove it from the DOM. However, if we have animation, we
- // need to wait for it to expire beforehand.
- // FIXME: this is a placeholder for a port of the transitions library.
- if ( ttScope.animation ) {
- if (!transitionTimeout) {
- transitionTimeout = $timeout(removeTooltip, 500);
+ ttScope.$evalAsync(function() {
+ if (ttScope) {
+ ttScope.isOpen = false;
+ assignIsOpen(false);
+ // And now we remove it from the DOM. However, if we have animation, we
+ // need to wait for it to expire beforehand.
+ // FIXME: this is a placeholder for a port of the transitions library.
+ // The fade transition in TWBS is 150ms.
+ if (ttScope.animation) {
+ if (!transitionTimeout) {
+ transitionTimeout = $timeout(removeTooltip, 150, false);
+ }
+ } else {
+ removeTooltip();
+ }
}
- } else {
- removeTooltip();
+ });
+ }
+
+ function cancelHide() {
+ if (hideTimeout) {
+ $timeout.cancel(hideTimeout);
+ hideTimeout = null;
+ }
+
+ if (transitionTimeout) {
+ $timeout.cancel(transitionTimeout);
+ transitionTimeout = null;
}
}
function createTooltip() {
// There can only be one tooltip element per directive shown at once.
if (tooltip) {
- removeTooltip();
+ return;
}
+
tooltipLinkedScope = ttScope.$new();
- tooltip = tooltipLinker(tooltipLinkedScope, function (tooltip) {
- if ( appendToBody ) {
- $document.find( 'body' ).append( tooltip );
+ tooltip = tooltipLinker(tooltipLinkedScope, function(tooltip) {
+ if (appendToBody) {
+ $document.find('body').append(tooltip);
} else {
- element.after( tooltip );
+ element.after(tooltip);
}
});
- tooltipLinkedScope.$watch(function () {
- $timeout(positionTooltip, 0, false);
+ openedTooltips.add(ttScope, {
+ close: hide
});
- if (options.useContentExp) {
- tooltipLinkedScope.$watch('contentExp()', function (val) {
- if (!val && ttScope.isOpen ) {
- hide();
- }
- });
- }
+ prepObservers();
}
function removeTooltip() {
- transitionTimeout = null;
+ cancelShow();
+ cancelHide();
+ unregisterObservers();
+
if (tooltip) {
tooltip.remove();
+
tooltip = null;
+ if (adjustmentTimeout) {
+ $timeout.cancel(adjustmentTimeout);
+ }
}
+
+ openedTooltips.remove(ttScope);
+
if (tooltipLinkedScope) {
tooltipLinkedScope.$destroy();
tooltipLinkedScope = null;
}
}
+ /**
+ * Set the initial scope values. Once
+ * the tooltip is created, the observers
+ * will be added to keep things in sync.
+ */
function prepareTooltip() {
- prepPopupClass();
- prepPlacement();
- prepPopupDelay();
+ ttScope.title = attrs[prefix + 'Title'];
+ if (contentParse) {
+ ttScope.content = contentParse(scope);
+ } else {
+ ttScope.content = attrs[ttType];
+ }
+
+ ttScope.popupClass = attrs[prefix + 'Class'];
+ ttScope.placement = angular.isDefined(attrs[prefix + 'Placement']) ? attrs[prefix + 'Placement'] : options.placement;
+ var placement = $position.parsePlacement(ttScope.placement);
+ lastPlacement = placement[1] ? placement[0] + '-' + placement[1] : placement[0];
+
+ var delay = parseInt(attrs[prefix + 'PopupDelay'], 10);
+ var closeDelay = parseInt(attrs[prefix + 'PopupCloseDelay'], 10);
+ ttScope.popupDelay = !isNaN(delay) ? delay : options.popupDelay;
+ ttScope.popupCloseDelay = !isNaN(closeDelay) ? closeDelay : options.popupCloseDelay;
}
- ttScope.contentExp = function () {
- return scope.$eval(attrs[type]);
+ function assignIsOpen(isOpen) {
+ if (isOpenParse && angular.isFunction(isOpenParse.assign)) {
+ isOpenParse.assign(scope, isOpen);
+ }
+ }
+
+ ttScope.contentExp = function() {
+ return ttScope.content;
};
/**
* Observe the relevant attributes.
*/
- if (!options.useContentExp) {
- attrs.$observe( type, function ( val ) {
- ttScope.content = val;
+ attrs.$observe('disabled', function(val) {
+ if (val) {
+ cancelShow();
+ }
- if (!val && ttScope.isOpen ) {
- hide();
+ if (val && ttScope.isOpen) {
+ hide();
+ }
+ });
+
+ if (isOpenParse) {
+ scope.$watch(isOpenParse, function(val) {
+ if (ttScope && !val === ttScope.isOpen) {
+ toggleTooltipBind();
}
});
}
- attrs.$observe( 'disabled', function ( val ) {
- if (val && ttScope.isOpen ) {
- hide();
+ function prepObservers() {
+ observers.length = 0;
+
+ if (contentParse) {
+ observers.push(
+ scope.$watch(contentParse, function(val) {
+ ttScope.content = val;
+ if (!val && ttScope.isOpen) {
+ hide();
+ }
+ })
+ );
+
+ observers.push(
+ tooltipLinkedScope.$watch(function() {
+ if (!repositionScheduled) {
+ repositionScheduled = true;
+ tooltipLinkedScope.$$postDigest(function() {
+ repositionScheduled = false;
+ if (ttScope && ttScope.isOpen) {
+ positionTooltip();
+ }
+ });
+ }
+ })
+ );
+ } else {
+ observers.push(
+ attrs.$observe(ttType, function(val) {
+ ttScope.content = val;
+ if (!val && ttScope.isOpen) {
+ hide();
+ } else {
+ positionTooltip();
+ }
+ })
+ );
}
- });
- attrs.$observe( prefix+'Title', function ( val ) {
- ttScope.title = val;
- });
+ observers.push(
+ attrs.$observe(prefix + 'Title', function(val) {
+ ttScope.title = val;
+ if (ttScope.isOpen) {
+ positionTooltip();
+ }
+ })
+ );
+
+ observers.push(
+ attrs.$observe(prefix + 'Placement', function(val) {
+ ttScope.placement = val ? val : options.placement;
+ if (ttScope.isOpen) {
+ positionTooltip();
+ }
+ })
+ );
+ }
- function prepPopupClass() {
- ttScope.popupClass = attrs[prefix + 'Class'];
+ function unregisterObservers() {
+ if (observers.length) {
+ angular.forEach(observers, function(observer) {
+ observer();
+ });
+ observers.length = 0;
+ }
}
- function prepPlacement() {
- var val = attrs[ prefix + 'Placement' ];
- ttScope.placement = angular.isDefined( val ) ? val : options.placement;
+ // hide tooltips/popovers for outsideClick trigger
+ function bodyHideTooltipBind(e) {
+ if (!ttScope || !ttScope.isOpen || !tooltip) {
+ return;
+ }
+ // make sure the tooltip/popover link or tool tooltip/popover itself were not clicked
+ if (!element[0].contains(e.target) && !tooltip[0].contains(e.target)) {
+ hideTooltipBind();
+ }
}
- function prepPopupDelay() {
- var val = attrs[ prefix + 'PopupDelay' ];
- var delay = parseInt( val, 10 );
- ttScope.popupDelay = ! isNaN(delay) ? delay : options.popupDelay;
+ // KeyboardEvent handler to hide the tooltip on Escape key press
+ function hideOnEscapeKey(e) {
+ if (e.which === 27) {
+ hideTooltipBind();
+ }
}
- var unregisterTriggers = function () {
- element.unbind(triggers.show, showTooltipBind);
- element.unbind(triggers.hide, hideTooltipBind);
+ var unregisterTriggers = function() {
+ triggers.show.forEach(function(trigger) {
+ if (trigger === 'outsideClick') {
+ element.off('click', toggleTooltipBind);
+ } else {
+ element.off(trigger, showTooltipBind);
+ element.off(trigger, toggleTooltipBind);
+ }
+ element.off('keypress', hideOnEscapeKey);
+ });
+ triggers.hide.forEach(function(trigger) {
+ if (trigger === 'outsideClick') {
+ $document.off('click', bodyHideTooltipBind);
+ } else {
+ element.off(trigger, hideTooltipBind);
+ }
+ });
};
function prepTriggers() {
- var val = attrs[ prefix + 'Trigger' ];
+ var showTriggers = [], hideTriggers = [];
+ var val = scope.$eval(attrs[prefix + 'Trigger']);
unregisterTriggers();
- triggers = getTriggers( val );
-
- if ( triggers.show === triggers.hide ) {
- element.bind( triggers.show, toggleTooltipBind );
+ if (angular.isObject(val)) {
+ Object.keys(val).forEach(function(key) {
+ showTriggers.push(key);
+ hideTriggers.push(val[key]);
+ });
+ triggers = {
+ show: showTriggers,
+ hide: hideTriggers
+ };
} else {
- element.bind( triggers.show, showTooltipBind );
- element.bind( triggers.hide, hideTooltipBind );
+ triggers = getTriggers(val);
+ }
+
+ if (triggers.show !== 'none') {
+ triggers.show.forEach(function(trigger, idx) {
+ if (trigger === 'outsideClick') {
+ element.on('click', toggleTooltipBind);
+ $document.on('click', bodyHideTooltipBind);
+ } else if (trigger === triggers.hide[idx]) {
+ element.on(trigger, toggleTooltipBind);
+ } else if (trigger) {
+ element.on(trigger, showTooltipBind);
+ element.on(triggers.hide[idx], hideTooltipBind);
+ }
+ element.on('keypress', hideOnEscapeKey);
+ });
}
}
+
prepTriggers();
var animation = scope.$eval(attrs[prefix + 'Animation']);
ttScope.animation = angular.isDefined(animation) ? !!animation : options.animation;
- var appendToBodyVal = scope.$eval(attrs[prefix + 'AppendToBody']);
- appendToBody = angular.isDefined(appendToBodyVal) ? appendToBodyVal : appendToBody;
-
- // if a tooltip is attached to we need to remove it on
- // location change as its parent scope will probably not be destroyed
- // by the change.
- if ( appendToBody ) {
- scope.$on('$locationChangeSuccess', function closeTooltipOnLocationChangeSuccess () {
- if ( ttScope.isOpen ) {
- hide();
- }
- });
+ var appendToBodyVal;
+ var appendKey = prefix + 'AppendToBody';
+ if (appendKey in attrs && attrs[appendKey] === undefined) {
+ appendToBodyVal = true;
+ } else {
+ appendToBodyVal = scope.$eval(attrs[appendKey]);
}
+ appendToBody = angular.isDefined(appendToBodyVal) ? appendToBodyVal : appendToBody;
+
// Make sure tooltip is destroyed and removed.
scope.$on('$destroy', function onDestroyTooltip() {
- $timeout.cancel( transitionTimeout );
- $timeout.cancel( popupTimeout );
unregisterTriggers();
removeTooltip();
ttScope = null;
@@ -372,11 +585,11 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap
})
// This is mostly ngInclude code but with a custom scope
-.directive( 'tooltipTemplateTransclude', [
+.directive('uibTooltipTemplateTransclude', [
'$animate', '$sce', '$compile', '$templateRequest',
-function ($animate , $sce , $compile , $templateRequest) {
+function ($animate, $sce, $compile, $templateRequest) {
return {
- link: function ( scope, elem, attrs ) {
+ link: function(scope, elem, attrs) {
var origScope = scope.$eval(attrs.tooltipTemplateTranscludeScope);
var changeCounter = 0,
@@ -389,10 +602,12 @@ function ($animate , $sce , $compile , $templateRequest) {
previousElement.remove();
previousElement = null;
}
+
if (currentScope) {
currentScope.$destroy();
currentScope = null;
}
+
if (currentElement) {
$animate.leave(currentElement).then(function() {
previousElement = null;
@@ -402,7 +617,7 @@ function ($animate , $sce , $compile , $templateRequest) {
}
};
- scope.$watch($sce.parseAsResourceUrl(attrs.tooltipTemplateTransclude), function (src) {
+ scope.$watch($sce.parseAsResourceUrl(attrs.uibTooltipTemplateTransclude), function(src) {
var thisChangeId = ++changeCounter;
if (src) {
@@ -444,85 +659,67 @@ function ($animate , $sce , $compile , $templateRequest) {
* They must not be animated as they're expected to be present on the tooltip on
* initialization.
*/
-.directive('tooltipClasses', function () {
+.directive('uibTooltipClasses', ['$uibPosition', function($uibPosition) {
return {
restrict: 'A',
- link: function (scope, element, attrs) {
+ link: function(scope, element, attrs) {
+ // need to set the primary position so the
+ // arrow has space during position measure.
+ // tooltip.positionTooltip()
if (scope.placement) {
- element.addClass(scope.placement);
+ // // There are no top-left etc... classes
+ // // in TWBS, so we need the primary position.
+ var position = $uibPosition.parsePlacement(scope.placement);
+ element.addClass(position[0]);
}
+
if (scope.popupClass) {
element.addClass(scope.popupClass);
}
- if (scope.animation()) {
+
+ if (scope.animation) {
element.addClass(attrs.tooltipAnimationClass);
}
}
};
-})
+}])
-.directive( 'tooltipPopup', function () {
+.directive('uibTooltipPopup', function() {
return {
- restrict: 'EA',
- replace: true,
- scope: { content: '@', placement: '@', popupClass: '@', animation: '&', isOpen: '&' },
- templateUrl: 'template/tooltip/tooltip-popup.html'
+ restrict: 'A',
+ scope: { content: '@' },
+ templateUrl: 'uib/template/tooltip/tooltip-popup.html'
};
})
-.directive( 'tooltip', [ '$tooltip', function ( $tooltip ) {
- return $tooltip( 'tooltip', 'tooltip', 'mouseenter' );
+.directive('uibTooltip', [ '$uibTooltip', function($uibTooltip) {
+ return $uibTooltip('uibTooltip', 'tooltip', 'mouseenter');
}])
-.directive( 'tooltipTemplatePopup', function () {
+.directive('uibTooltipTemplatePopup', function() {
return {
- restrict: 'EA',
- replace: true,
- scope: { contentExp: '&', placement: '@', popupClass: '@', animation: '&', isOpen: '&',
- originScope: '&' },
- templateUrl: 'template/tooltip/tooltip-template-popup.html'
+ restrict: 'A',
+ scope: { contentExp: '&', originScope: '&' },
+ templateUrl: 'uib/template/tooltip/tooltip-template-popup.html'
};
})
-.directive( 'tooltipTemplate', [ '$tooltip', function ( $tooltip ) {
- return $tooltip('tooltipTemplate', 'tooltip', 'mouseenter', {
+.directive('uibTooltipTemplate', ['$uibTooltip', function($uibTooltip) {
+ return $uibTooltip('uibTooltipTemplate', 'tooltip', 'mouseenter', {
useContentExp: true
});
}])
-.directive( 'tooltipHtmlPopup', function () {
+.directive('uibTooltipHtmlPopup', function() {
return {
- restrict: 'EA',
- replace: true,
- scope: { contentExp: '&', placement: '@', popupClass: '@', animation: '&', isOpen: '&' },
- templateUrl: 'template/tooltip/tooltip-html-popup.html'
+ restrict: 'A',
+ scope: { contentExp: '&' },
+ templateUrl: 'uib/template/tooltip/tooltip-html-popup.html'
};
})
-.directive( 'tooltipHtml', [ '$tooltip', function ( $tooltip ) {
- return $tooltip('tooltipHtml', 'tooltip', 'mouseenter', {
+.directive('uibTooltipHtml', ['$uibTooltip', function($uibTooltip) {
+ return $uibTooltip('uibTooltipHtml', 'tooltip', 'mouseenter', {
useContentExp: true
});
-}])
-
-/*
-Deprecated
-*/
-.directive( 'tooltipHtmlUnsafePopup', function () {
- return {
- restrict: 'EA',
- replace: true,
- scope: { content: '@', placement: '@', popupClass: '@', animation: '&', isOpen: '&' },
- templateUrl: 'template/tooltip/tooltip-html-unsafe-popup.html'
- };
-})
-
-.value('tooltipHtmlUnsafeSuppressDeprecated', false)
-.directive( 'tooltipHtmlUnsafe', [
- '$tooltip', 'tooltipHtmlUnsafeSuppressDeprecated', '$log',
-function ( $tooltip , tooltipHtmlUnsafeSuppressDeprecated , $log) {
- if (!tooltipHtmlUnsafeSuppressDeprecated) {
- $log.warn('tooltip-html-unsafe is now deprecated. Use tooltip-html or tooltip-template instead.');
- }
- return $tooltip( 'tooltipHtmlUnsafe', 'tooltip', 'mouseenter' );
}]);
diff --git a/src/transition/test/transition.spec.js b/src/transition/test/transition.spec.js
deleted file mode 100644
index aafe1bfb2a..0000000000
--- a/src/transition/test/transition.spec.js
+++ /dev/null
@@ -1,147 +0,0 @@
-
-describe('$transition', function() {
-
- // Work out if we are running IE
- var ie = (function(){
- var v = 3,
- div = document.createElement('div'),
- all = div.getElementsByTagName('i');
- do {
- div.innerHTML = '';
- } while(all[0]);
- return v > 4 ? v : undefined;
- }());
-
- var $transition, $timeout;
-
- beforeEach(module('ui.bootstrap.transition'));
-
- beforeEach(inject(function(_$transition_, _$timeout_) {
- $transition = _$transition_;
- $timeout = _$timeout_;
- }));
-
- it('returns our custom promise', function() {
- var element = angular.element('');
- var promise = $transition(element, '');
- expect(promise.then).toEqual(jasmine.any(Function));
- expect(promise.cancel).toEqual(jasmine.any(Function));
- });
-
- it('changes the css if passed a string', function() {
- var element = angular.element('');
- spyOn(element, 'addClass');
- $transition(element, 'triggerClass');
- $timeout.flush();
-
- expect(element.addClass).toHaveBeenCalledWith('triggerClass');
- });
-
- it('changes the style if passed an object', function() {
- var element = angular.element('');
- var triggerStyle = { height: '11px' };
- spyOn(element, 'css');
- $transition(element, triggerStyle);
- $timeout.flush();
- expect(element.css).toHaveBeenCalledWith(triggerStyle);
- });
-
- it('calls the function if passed', function() {
- var element = angular.element('');
- var triggerFunction = jasmine.createSpy('triggerFunction');
- $transition(element, triggerFunction);
- $timeout.flush();
- expect(triggerFunction).toHaveBeenCalledWith(element);
- });
-
- // Versions of Internet Explorer before version 10 do not have CSS transitions
- if ( !ie || ie > 9 ) {
- describe('transitionEnd event', function() {
- var element, triggerTransitionEnd;
-
- beforeEach(function() {
- element = angular.element('');
- // Mock up the element.bind method
- spyOn(element, 'bind').and.callFake(function(element, handler) {
- // Store the handler to be used to simulate the end of the transition later
- triggerTransitionEnd = handler;
- });
- // Mock up the element.unbind method
- spyOn(element, 'unbind');
- });
-
- describe('transitionEndEventName', function() {
- it('should be a string ending with transitionend', function() {
- expect($transition.transitionEndEventName).toMatch(/transitionend$/i);
- });
- });
-
- describe('animationEndEventName', function() {
- it('should be a string ending with animationend', function() {
- expect($transition.animationEndEventName).toMatch(/animationend$/i);
- });
- });
-
- it('binds a transitionEnd handler to the element', function() {
- $transition(element, '');
- expect(element.bind).toHaveBeenCalledWith($transition.transitionEndEventName, jasmine.any(Function));
- });
-
- it('binds an animationEnd handler to the element if option is given', function() {
- $transition(element, '', {animation: true});
- expect(element.bind).toHaveBeenCalledWith($transition.animationEndEventName, jasmine.any(Function));
- });
-
- it('resolves the promise when the transitionEnd is triggered', function() {
- var resolutionHandler = jasmine.createSpy('resolutionHandler');
-
- // Run the transition
- $transition(element, '').then(resolutionHandler);
-
- // Simulate the end of transition event
- triggerTransitionEnd();
- $timeout.flush();
-
- expect(resolutionHandler).toHaveBeenCalledWith(element);
- });
-
- it('rejects the promise if transition is cancelled', function() {
- var rejectionHandler = jasmine.createSpy('rejectionHandler');
-
- var promise = $transition(element, '');
- promise.then(null, rejectionHandler);
-
- promise.cancel();
- inject(function($rootScope) {
- $rootScope.$digest();
- });
- expect(rejectionHandler).toHaveBeenCalledWith(jasmine.any(String));
- expect(element.unbind).toHaveBeenCalledWith($transition.transitionEndEventName, jasmine.any(Function));
- });
- });
- } else {
-
- describe('transitionEndEventName', function() {
- it('should be undefined', function() {
- expect($transition.transitionEndEventName).not.toBeDefined();
- });
- });
-
- it('does not bind a transitionEnd handler to the element', function() {
- var element = angular.element('');
- spyOn(element, 'bind');
- $transition(element, '');
- expect(element.bind).not.toHaveBeenCalledWith($transition.transitionEndEventName, jasmine.any(Function));
- });
-
- it('resolves the promise', function() {
- var element = angular.element('');
- var transitionEndHandler = jasmine.createSpy('transitionEndHandler');
- $transition(element, '').then(transitionEndHandler);
- $timeout.flush();
- expect(transitionEndHandler).toHaveBeenCalledWith(element);
- });
-
- }
-});
-
diff --git a/src/transition/transition.js b/src/transition/transition.js
deleted file mode 100644
index 490cdf7bee..0000000000
--- a/src/transition/transition.js
+++ /dev/null
@@ -1,89 +0,0 @@
-angular.module('ui.bootstrap.transition', [])
-
-.value('$transitionSuppressDeprecated', false)
-/**
- * $transition service provides a consistent interface to trigger CSS 3 transitions and to be informed when they complete.
- * @param {DOMElement} element The DOMElement that will be animated.
- * @param {string|object|function} trigger The thing that will cause the transition to start:
- * - As a string, it represents the css class to be added to the element.
- * - As an object, it represents a hash of style attributes to be applied to the element.
- * - As a function, it represents a function to be called that will cause the transition to occur.
- * @return {Promise} A promise that is resolved when the transition finishes.
- */
-.factory('$transition', [
- '$q', '$timeout', '$rootScope', '$log', '$transitionSuppressDeprecated',
-function($q , $timeout , $rootScope , $log , $transitionSuppressDeprecated) {
-
- if (!$transitionSuppressDeprecated) {
- $log.warn('$transition is now deprecated. Use $animate from ngAnimate instead.');
- }
-
- var $transition = function(element, trigger, options) {
- options = options || {};
- var deferred = $q.defer();
- var endEventName = $transition[options.animation ? 'animationEndEventName' : 'transitionEndEventName'];
-
- var transitionEndHandler = function(event) {
- $rootScope.$apply(function() {
- element.unbind(endEventName, transitionEndHandler);
- deferred.resolve(element);
- });
- };
-
- if (endEventName) {
- element.bind(endEventName, transitionEndHandler);
- }
-
- // Wrap in a timeout to allow the browser time to update the DOM before the transition is to occur
- $timeout(function() {
- if ( angular.isString(trigger) ) {
- element.addClass(trigger);
- } else if ( angular.isFunction(trigger) ) {
- trigger(element);
- } else if ( angular.isObject(trigger) ) {
- element.css(trigger);
- }
- //If browser does not support transitions, instantly resolve
- if ( !endEventName ) {
- deferred.resolve(element);
- }
- });
-
- // Add our custom cancel function to the promise that is returned
- // We can call this if we are about to run a new transition, which we know will prevent this transition from ending,
- // i.e. it will therefore never raise a transitionEnd event for that transition
- deferred.promise.cancel = function() {
- if ( endEventName ) {
- element.unbind(endEventName, transitionEndHandler);
- }
- deferred.reject('Transition cancelled');
- };
-
- return deferred.promise;
- };
-
- // Work out the name of the transitionEnd event
- var transElement = document.createElement('trans');
- var transitionEndEventNames = {
- 'WebkitTransition': 'webkitTransitionEnd',
- 'MozTransition': 'transitionend',
- 'OTransition': 'oTransitionEnd',
- 'transition': 'transitionend'
- };
- var animationEndEventNames = {
- 'WebkitTransition': 'webkitAnimationEnd',
- 'MozTransition': 'animationend',
- 'OTransition': 'oAnimationEnd',
- 'transition': 'animationend'
- };
- function findEndEventName(endEventNames) {
- for (var name in endEventNames){
- if (transElement.style[name] !== undefined) {
- return endEventNames[name];
- }
- }
- }
- $transition.transitionEndEventName = findEndEventName(transitionEndEventNames);
- $transition.animationEndEventName = findEndEventName(animationEndEventNames);
- return $transition;
-}]);
diff --git a/src/typeahead/docs/demo.html b/src/typeahead/docs/demo.html
index b0d4b1967d..34faf743fc 100644
--- a/src/typeahead/docs/demo.html
+++ b/src/typeahead/docs/demo.html
@@ -1,21 +1,78 @@
+
+
-
+
+
+
+
Static arrays
Model: {{selected | json}}
-
+
Asynchronous results
Model: {{asyncSelected | json}}
-
+
+
+ No Results Found
+
+
+
ngModelOptions support
+
Model: {{ngModelOptionsSelected | json}}
+
Custom templates for results
Model: {{customSelected | json}}
-
+
+
+
Custom popup templates for typeahead's dropdown
+
Model: {{customPopupSelected | json}}
+
diff --git a/src/typeahead/docs/demo.js b/src/typeahead/docs/demo.js
index 0148ec421a..bc06c53aa2 100644
--- a/src/typeahead/docs/demo.js
+++ b/src/typeahead/docs/demo.js
@@ -1,5 +1,7 @@
angular.module('ui.bootstrap.demo').controller('TypeaheadCtrl', function($scope, $http) {
+ var _selected;
+
$scope.selected = undefined;
$scope.states = ['Alabama', 'Alaska', 'Arizona', 'Arkansas', 'California', 'Colorado', 'Connecticut', 'Delaware', 'Florida', 'Georgia', 'Hawaii', 'Idaho', 'Illinois', 'Indiana', 'Iowa', 'Kansas', 'Kentucky', 'Louisiana', 'Maine', 'Maryland', 'Massachusetts', 'Michigan', 'Minnesota', 'Mississippi', 'Missouri', 'Montana', 'Nebraska', 'Nevada', 'New Hampshire', 'New Jersey', 'New Mexico', 'New York', 'North Dakota', 'North Carolina', 'Ohio', 'Oklahoma', 'Oregon', 'Pennsylvania', 'Rhode Island', 'South Carolina', 'South Dakota', 'Tennessee', 'Texas', 'Utah', 'Vermont', 'Virginia', 'Washington', 'West Virginia', 'Wisconsin', 'Wyoming'];
// Any function returning a promise object can be used to load values asynchronously
@@ -16,5 +18,21 @@ angular.module('ui.bootstrap.demo').controller('TypeaheadCtrl', function($scope,
});
};
+ $scope.ngModelOptionsSelected = function(value) {
+ if (arguments.length) {
+ _selected = value;
+ } else {
+ return _selected;
+ }
+ };
+
+ $scope.modelOptions = {
+ debounce: {
+ default: 500,
+ blur: 250
+ },
+ getterSetter: true
+ };
+
$scope.statesWithFlags = [{'name':'Alabama','flag':'5/5c/Flag_of_Alabama.svg/45px-Flag_of_Alabama.svg.png'},{'name':'Alaska','flag':'e/e6/Flag_of_Alaska.svg/43px-Flag_of_Alaska.svg.png'},{'name':'Arizona','flag':'9/9d/Flag_of_Arizona.svg/45px-Flag_of_Arizona.svg.png'},{'name':'Arkansas','flag':'9/9d/Flag_of_Arkansas.svg/45px-Flag_of_Arkansas.svg.png'},{'name':'California','flag':'0/01/Flag_of_California.svg/45px-Flag_of_California.svg.png'},{'name':'Colorado','flag':'4/46/Flag_of_Colorado.svg/45px-Flag_of_Colorado.svg.png'},{'name':'Connecticut','flag':'9/96/Flag_of_Connecticut.svg/39px-Flag_of_Connecticut.svg.png'},{'name':'Delaware','flag':'c/c6/Flag_of_Delaware.svg/45px-Flag_of_Delaware.svg.png'},{'name':'Florida','flag':'f/f7/Flag_of_Florida.svg/45px-Flag_of_Florida.svg.png'},{'name':'Georgia','flag':'5/54/Flag_of_Georgia_%28U.S._state%29.svg/46px-Flag_of_Georgia_%28U.S._state%29.svg.png'},{'name':'Hawaii','flag':'e/ef/Flag_of_Hawaii.svg/46px-Flag_of_Hawaii.svg.png'},{'name':'Idaho','flag':'a/a4/Flag_of_Idaho.svg/38px-Flag_of_Idaho.svg.png'},{'name':'Illinois','flag':'0/01/Flag_of_Illinois.svg/46px-Flag_of_Illinois.svg.png'},{'name':'Indiana','flag':'a/ac/Flag_of_Indiana.svg/45px-Flag_of_Indiana.svg.png'},{'name':'Iowa','flag':'a/aa/Flag_of_Iowa.svg/44px-Flag_of_Iowa.svg.png'},{'name':'Kansas','flag':'d/da/Flag_of_Kansas.svg/46px-Flag_of_Kansas.svg.png'},{'name':'Kentucky','flag':'8/8d/Flag_of_Kentucky.svg/46px-Flag_of_Kentucky.svg.png'},{'name':'Louisiana','flag':'e/e0/Flag_of_Louisiana.svg/46px-Flag_of_Louisiana.svg.png'},{'name':'Maine','flag':'3/35/Flag_of_Maine.svg/45px-Flag_of_Maine.svg.png'},{'name':'Maryland','flag':'a/a0/Flag_of_Maryland.svg/45px-Flag_of_Maryland.svg.png'},{'name':'Massachusetts','flag':'f/f2/Flag_of_Massachusetts.svg/46px-Flag_of_Massachusetts.svg.png'},{'name':'Michigan','flag':'b/b5/Flag_of_Michigan.svg/45px-Flag_of_Michigan.svg.png'},{'name':'Minnesota','flag':'b/b9/Flag_of_Minnesota.svg/46px-Flag_of_Minnesota.svg.png'},{'name':'Mississippi','flag':'4/42/Flag_of_Mississippi.svg/45px-Flag_of_Mississippi.svg.png'},{'name':'Missouri','flag':'5/5a/Flag_of_Missouri.svg/46px-Flag_of_Missouri.svg.png'},{'name':'Montana','flag':'c/cb/Flag_of_Montana.svg/45px-Flag_of_Montana.svg.png'},{'name':'Nebraska','flag':'4/4d/Flag_of_Nebraska.svg/46px-Flag_of_Nebraska.svg.png'},{'name':'Nevada','flag':'f/f1/Flag_of_Nevada.svg/45px-Flag_of_Nevada.svg.png'},{'name':'New Hampshire','flag':'2/28/Flag_of_New_Hampshire.svg/45px-Flag_of_New_Hampshire.svg.png'},{'name':'New Jersey','flag':'9/92/Flag_of_New_Jersey.svg/45px-Flag_of_New_Jersey.svg.png'},{'name':'New Mexico','flag':'c/c3/Flag_of_New_Mexico.svg/45px-Flag_of_New_Mexico.svg.png'},{'name':'New York','flag':'1/1a/Flag_of_New_York.svg/46px-Flag_of_New_York.svg.png'},{'name':'North Carolina','flag':'b/bb/Flag_of_North_Carolina.svg/45px-Flag_of_North_Carolina.svg.png'},{'name':'North Dakota','flag':'e/ee/Flag_of_North_Dakota.svg/38px-Flag_of_North_Dakota.svg.png'},{'name':'Ohio','flag':'4/4c/Flag_of_Ohio.svg/46px-Flag_of_Ohio.svg.png'},{'name':'Oklahoma','flag':'6/6e/Flag_of_Oklahoma.svg/45px-Flag_of_Oklahoma.svg.png'},{'name':'Oregon','flag':'b/b9/Flag_of_Oregon.svg/46px-Flag_of_Oregon.svg.png'},{'name':'Pennsylvania','flag':'f/f7/Flag_of_Pennsylvania.svg/45px-Flag_of_Pennsylvania.svg.png'},{'name':'Rhode Island','flag':'f/f3/Flag_of_Rhode_Island.svg/32px-Flag_of_Rhode_Island.svg.png'},{'name':'South Carolina','flag':'6/69/Flag_of_South_Carolina.svg/45px-Flag_of_South_Carolina.svg.png'},{'name':'South Dakota','flag':'1/1a/Flag_of_South_Dakota.svg/46px-Flag_of_South_Dakota.svg.png'},{'name':'Tennessee','flag':'9/9e/Flag_of_Tennessee.svg/46px-Flag_of_Tennessee.svg.png'},{'name':'Texas','flag':'f/f7/Flag_of_Texas.svg/45px-Flag_of_Texas.svg.png'},{'name':'Utah','flag':'f/f6/Flag_of_Utah.svg/45px-Flag_of_Utah.svg.png'},{'name':'Vermont','flag':'4/49/Flag_of_Vermont.svg/46px-Flag_of_Vermont.svg.png'},{'name':'Virginia','flag':'4/47/Flag_of_Virginia.svg/44px-Flag_of_Virginia.svg.png'},{'name':'Washington','flag':'5/54/Flag_of_Washington.svg/46px-Flag_of_Washington.svg.png'},{'name':'West Virginia','flag':'2/22/Flag_of_West_Virginia.svg/46px-Flag_of_West_Virginia.svg.png'},{'name':'Wisconsin','flag':'2/22/Flag_of_Wisconsin.svg/45px-Flag_of_Wisconsin.svg.png'},{'name':'Wyoming','flag':'b/bc/Flag_of_Wyoming.svg/43px-Flag_of_Wyoming.svg.png'}];
});
diff --git a/src/typeahead/docs/readme.md b/src/typeahead/docs/readme.md
index 621415c565..efd2dfd0fc 100644
--- a/src/typeahead/docs/readme.md
+++ b/src/typeahead/docs/readme.md
@@ -11,47 +11,116 @@ The `sourceArray` expression can use a special `$viewValue` variable that corres
This directive works with promises, meaning you can retrieve matches using the `$http` service with minimal effort.
-The typeahead directives provide several attributes:
-
-* `ng-model`
- :
- Assignable angular expression to data-bind to
-
-* `typeahead`
- :
- Comprehension Angular expression (see [select directive](http://docs.angularjs.org/api/ng.directive:select))
-
-* `typeahead-append-to-body`
- _(Defaults: false)_ : Should the typeahead popup be appended to $body instead of the parent element?
-
-* `typeahead-editable`
- _(Defaults: true)_ :
- Should it restrict model values to the ones selected from the popup only ?
-
-* `typeahead-input-formatter`
- _(Defaults: undefined)_ :
- Format the ng-model result after selection
-
-* `typeahead-loading`
- _(Defaults: angular.noop)_ :
- Binding to a variable that indicates if matches are being retrieved asynchronously
-
-* `typeahead-min-length`
- _(Defaults: 1)_ :
- Minimal no of characters that needs to be entered before typeahead kicks-in
-
-* `typeahead-on-select($item, $model, $label)`
- _(Defaults: null)_ :
- A callback executed when a match is selected
-
-* `typeahead-template-url`
- :
- Set custom item template
-
-* `typeahead-wait-ms`
- _(Defaults: 0)_ :
- Minimal wait time after last character typed before typeahead kicks-in
+### uib-typeahead settings
+
+* `ng-model`
+ $
+ -
+ Assignable angular expression to data-bind to.
+
+* `ng-model-options`
+ $ -
+ Options for ng-model (see [ng-model-options directive](https://docs.angularjs.org/api/ng/directive/ngModelOptions)). Currently supports the `debounce` and `getterSetter` options.
+
+* `typeahead-append-to`
+ $
+ _(Default: `null`)_ -
+ Should the typeahead popup be appended to an element instead of the parent element?
+
+* `typeahead-append-to-body`
+ $
+
+ _(Default: `false`)_ -
+ Should the typeahead popup be appended to $body instead of the parent element?
+
+* `typeahead-editable`
+ $
+
+ _(Default: `true`)_ -
+ Should it restrict model values to the ones selected from the popup only?
* `typeahead-focus-first`
- _(Defaults: true)_ :
- Should the first match automatically be focused as you type?
+ $
+ _(Default: `true`)_ -
+ Should the first match automatically be focused as you type?
+
+* `typeahead-focus-on-select`
+ _(Default: `true`)_ -
+ On selection, focus the input element the typeahead directive is associated with.
+
+* `typeahead-input-formatter`
+
+ _(Default: `undefined`)_ -
+ Format the ng-model result after selection.
+
+* `typeahead-is-open`
+ $
+
+ _(Default: `angular.noop`)_ -
+ Binding to a variable that indicates if the dropdown is open.
+
+* `typeahead-loading`
+ $
+
+ _(Default: `angular.noop`)_ -
+ Binding to a variable that indicates if matches are being retrieved asynchronously.
+
+* `typeahead-min-length`
+ $
+
+ _(Default: `1`)_ -
+ Minimal no of characters that needs to be entered before typeahead kicks-in. Must be greater than or equal to 0.
+
+* `typeahead-no-results`
+ $
+
+ _(Default: `angular.noop`)_ -
+ Binding to a variable that indicates if no matching results were found.
+
+* `typeahead-should-select($event)`
+ $
+ _(Default: `null`)_ -
+ A callback executed when a `keyup` event that might trigger a selection occurs. Selection will only occur if this function returns true.
+
+* `typeahead-on-select($item, $model, $label, $event)`
+ $
+ _(Default: `null`)_ -
+ A callback executed when a match is selected. $event can be undefined if selection not triggered from a user event.
+
+* `typeahead-popup-template-url`
+ _(Default: `uib/template/typeahead/typeahead-popup.html`)_ -
+ Set custom popup template.
+
+* `typeahead-select-on-blur`
+ $
+ _(Default: `false`)_ -
+ On blur, select the currently highlighted match.
+
+* `typeahead-select-on-exact`
+ $
+ _(Default: `false`)_ -
+ Automatically select the item when it is the only one that exactly matches the user input.
+
+* `typeahead-show-hint`
+ $
+ _(Default: `false`)_ -
+ Show hint when the first option matches.
+
+* `typeahead-template-url`
+ _(Default: `uib/template/typeahead/typeahead-match.html`)_ -
+ Set custom item template.
+
+* `typeahead-wait-ms`
+ $
+
+ _(Default: `0`)_ -
+ Minimal wait time after last character typed before typeahead kicks-in.
+
+* `uib-typeahead`
+ $
+ -
+ Comprehension Angular expression (see [select directive](http://docs.angularjs.org/api/ng.directive:select)).
+
+**Notes**
+
+If a custom template for the popup is used, the wrapper selector used for the match items is the `uib-typeahead-match` class.
diff --git a/src/typeahead/index-nocss.js b/src/typeahead/index-nocss.js
new file mode 100644
index 0000000000..45f5d207be
--- /dev/null
+++ b/src/typeahead/index-nocss.js
@@ -0,0 +1,11 @@
+require('../debounce');
+require('../position/index-nocss.js');
+require('../../template/typeahead/typeahead-match.html.js');
+require('../../template/typeahead/typeahead-popup.html.js');
+require('./typeahead');
+
+var MODULE_NAME = 'ui.bootstrap.module.typeahead';
+
+angular.module(MODULE_NAME, ['ui.bootstrap.typeahead', 'uib/template/typeahead/typeahead-match.html', 'uib/template/typeahead/typeahead-popup.html']);
+
+module.exports = MODULE_NAME;
diff --git a/src/typeahead/index.js b/src/typeahead/index.js
new file mode 100644
index 0000000000..440045986e
--- /dev/null
+++ b/src/typeahead/index.js
@@ -0,0 +1,3 @@
+require('../position/position.css');
+require('./typeahead.css');
+module.exports = require('./index-nocss.js');
diff --git a/src/typeahead/test/typeahead-highlight-ngsanitize.spec.js b/src/typeahead/test/typeahead-highlight-ngsanitize.spec.js
new file mode 100644
index 0000000000..de24319361
--- /dev/null
+++ b/src/typeahead/test/typeahead-highlight-ngsanitize.spec.js
@@ -0,0 +1,16 @@
+describe('Security concerns', function() {
+ var highlightFilter, $sanitize, logSpy;
+
+ beforeEach(module('ui.bootstrap.typeahead', 'ngSanitize'));
+
+ beforeEach(inject(function (uibTypeaheadHighlightFilter, _$sanitize_, $log) {
+ highlightFilter = uibTypeaheadHighlightFilter;
+ $sanitize = _$sanitize_;
+ logSpy = spyOn($log, 'warn');
+ }));
+
+ it('should not call the $log service when ngSanitize is present', function() {
+ highlightFilter('before after', 'match');
+ expect(logSpy).not.toHaveBeenCalled();
+ });
+});
diff --git a/src/typeahead/test/typeahead-highlight.spec.js b/src/typeahead/test/typeahead-highlight.spec.js
index 1a5aafc48d..4488c6d4ea 100644
--- a/src/typeahead/test/typeahead-highlight.spec.js
+++ b/src/typeahead/test/typeahead-highlight.spec.js
@@ -1,39 +1,51 @@
describe('typeaheadHighlight', function () {
- var highlightFilter;
+ var highlightFilter, $log, $sce, logSpy;
beforeEach(module('ui.bootstrap.typeahead'));
- beforeEach(inject(function (typeaheadHighlightFilter) {
- highlightFilter = typeaheadHighlightFilter;
+
+ beforeEach(inject(function(_$log_, _$sce_) {
+ $log = _$log_;
+ $sce = _$sce_;
+ logSpy = spyOn($log, 'warn');
+ }));
+
+ beforeEach(inject(function(uibTypeaheadHighlightFilter) {
+ highlightFilter = uibTypeaheadHighlightFilter;
}));
- it('should higlight a match', function () {
- expect(highlightFilter('before match after', 'match')).toEqual('before match after');
+ it('should higlight a match', function() {
+ expect($sce.getTrustedHtml(highlightFilter('before match after', 'match'))).toEqual('before match after');
+ });
+
+ it('should higlight a match with mixed case', function() {
+ expect($sce.getTrustedHtml(highlightFilter('before MaTch after', 'match'))).toEqual('before MaTch after');
});
- it('should higlight a match with mixed case', function () {
- expect(highlightFilter('before MaTch after', 'match')).toEqual('before MaTch after');
+ it('should higlight all matches', function() {
+ expect($sce.getTrustedHtml(highlightFilter('before MaTch after match', 'match'))).toEqual('before MaTch after match');
});
- it('should higlight all matches', function () {
- expect(highlightFilter('before MaTch after match', 'match')).toEqual('before MaTch after match');
+ it('should do nothing if no match', function() {
+ expect($sce.getTrustedHtml(highlightFilter('before match after', 'nomatch'))).toEqual('before match after');
});
- it('should do nothing if no match', function () {
- expect(highlightFilter('before match after', 'nomatch')).toEqual('before match after');
+ it('should do nothing if no or empty query', function() {
+ expect($sce.getTrustedHtml(highlightFilter('before match after', ''))).toEqual('before match after');
+ expect($sce.getTrustedHtml(highlightFilter('before match after', null))).toEqual('before match after');
+ expect($sce.getTrustedHtml(highlightFilter('before match after', undefined))).toEqual('before match after');
});
- it('should do nothing if no or empty query', function () {
- expect(highlightFilter('before match after', '')).toEqual('before match after');
- expect(highlightFilter('before match after', null)).toEqual('before match after');
- expect(highlightFilter('before match after', undefined)).toEqual('before match after');
+ it('issue 316 - should work correctly for regexp reserved words', function() {
+ expect($sce.getTrustedHtml(highlightFilter('before (match after', '(match'))).toEqual('before (match after');
});
- it('issue 316 - should work correctly for regexp reserved words', function () {
- expect(highlightFilter('before (match after', '(match')).toEqual('before (match after');
+ it('issue 1777 - should work correctly with numeric values', function() {
+ expect($sce.getTrustedHtml(highlightFilter(123, '2'))).toEqual('123');
});
- it('issue 1777 - should work correctly with numeric values', function () {
- expect(highlightFilter(123, '2')).toEqual('123');
+ it('should show a warning when this component is being used unsafely', function() {
+ highlightFilter('before match after', 'match');
+ expect(logSpy).toHaveBeenCalled();
});
});
diff --git a/src/typeahead/test/typeahead-parser.spec.js b/src/typeahead/test/typeahead-parser.spec.js
index e6a7e536cd..d201b73644 100644
--- a/src/typeahead/test/typeahead-parser.spec.js
+++ b/src/typeahead/test/typeahead-parser.spec.js
@@ -1,15 +1,14 @@
-describe('syntax parser', function () {
-
+describe('syntax parser', function() {
var typeaheadParser, scope, filterFilter;
beforeEach(module('ui.bootstrap.typeahead'));
- beforeEach(inject(function (_$rootScope_, _filterFilter_, _typeaheadParser_) {
- typeaheadParser = _typeaheadParser_;
+ beforeEach(inject(function(_$rootScope_, _filterFilter_, uibTypeaheadParser) {
+ typeaheadParser = uibTypeaheadParser;
scope = _$rootScope_;
filterFilter = _filterFilter_;
}));
- it('should parse the simplest array-based syntax', function () {
+ it('should parse the simplest array-based syntax', function() {
scope.states = ['Alabama', 'California', 'Delaware'];
var result = typeaheadParser.parse('state for state in states | filter:$viewValue');
@@ -22,8 +21,8 @@ describe('syntax parser', function () {
expect(result.modelMapper(scope, locals)).toEqual('Alabama');
});
- it('should parse the simplest function-based syntax', function () {
- scope.getStates = function ($viewValue) {
+ it('should parse the simplest function-based syntax', function() {
+ scope.getStates = function($viewValue) {
return filterFilter(['Alabama', 'California', 'Delaware'], $viewValue);
};
var result = typeaheadParser.parse('state for state in getStates($viewValue)');
@@ -38,7 +37,6 @@ describe('syntax parser', function () {
});
it('should allow to specify custom model mapping that is used as a label as well', function () {
-
scope.states = [
{code:'AL', name:'Alabama'},
{code:'CA', name:'California'},
@@ -59,8 +57,7 @@ describe('syntax parser', function () {
expect(result.modelMapper(scope, locals)).toEqual('Alabama');
});
- it('should allow to specify custom view and model mappers', function () {
-
+ it('should allow to specify custom view and model mappers', function() {
scope.states = [
{code:'AL', name:'Alabama'},
{code:'CA', name:'California'},
diff --git a/src/typeahead/test/typeahead-popup.spec.js b/src/typeahead/test/typeahead-popup.spec.js
index 89c9f75e83..325d17a529 100644
--- a/src/typeahead/test/typeahead-popup.spec.js
+++ b/src/typeahead/test/typeahead-popup.spec.js
@@ -1,22 +1,20 @@
-describe('typeaheadPopup - result rendering', function () {
-
+describe('typeaheadPopup - result rendering', function() {
var scope, $rootScope, $compile;
beforeEach(module('ui.bootstrap.typeahead'));
- beforeEach(module('template/typeahead/typeahead-popup.html'));
- beforeEach(module('template/typeahead/typeahead-match.html'));
- beforeEach(inject(function (_$rootScope_, _$compile_) {
+ beforeEach(module('uib/template/typeahead/typeahead-popup.html'));
+ beforeEach(module('uib/template/typeahead/typeahead-match.html'));
+ beforeEach(inject(function(_$rootScope_, _$compile_) {
$rootScope = _$rootScope_;
scope = $rootScope.$new();
$compile = _$compile_;
}));
- it('should render initial results', function () {
-
+ it('should render initial results', function() {
scope.matches = ['foo', 'bar', 'baz'];
scope.active = 1;
- var el = $compile('
')(scope);
+ var el = $compile('
')(scope);
$rootScope.$digest();
var liElems = el.find('li');
@@ -26,12 +24,11 @@ describe('typeaheadPopup - result rendering', function () {
expect(liElems.eq(2)).not.toHaveClass('active');
});
- it('should change active item on mouseenter', function () {
-
+ it('should change active item on mouseenter', function() {
scope.matches = ['foo', 'bar', 'baz'];
scope.active = 1;
- var el = $compile('
')(scope);
+ var el = $compile('
')(scope);
$rootScope.$digest();
var liElems = el.find('li');
@@ -44,14 +41,13 @@ describe('typeaheadPopup - result rendering', function () {
expect(liElems.eq(2)).toHaveClass('active');
});
- it('should select an item on mouse click', function () {
-
+ it('should select an item on mouse click', function() {
scope.matches = ['foo', 'bar', 'baz'];
scope.active = 1;
$rootScope.select = angular.noop;
spyOn($rootScope, 'select');
- var el = $compile('
')(scope);
+ var el = $compile('
')(scope);
$rootScope.$digest();
var liElems = el.find('li');
diff --git a/src/typeahead/test/typeahead.spec.js b/src/typeahead/test/typeahead.spec.js
index 741621e65c..4423b8a66e 100644
--- a/src/typeahead/test/typeahead.spec.js
+++ b/src/typeahead/test/typeahead.spec.js
@@ -1,23 +1,23 @@
-describe('typeahead tests', function () {
-
- var $scope, $compile, $document, $timeout;
+describe('typeahead tests', function() {
+ var $scope, $compile, $document, $templateCache, $timeout, $window;
var changeInputValueTo;
beforeEach(module('ui.bootstrap.typeahead'));
- beforeEach(module('template/typeahead/typeahead-popup.html'));
- beforeEach(module('template/typeahead/typeahead-match.html'));
+ beforeEach(module('ngSanitize'));
+ beforeEach(module('uib/template/typeahead/typeahead-popup.html'));
+ beforeEach(module('uib/template/typeahead/typeahead-match.html'));
beforeEach(module(function($compileProvider) {
- $compileProvider.directive('formatter', function () {
+ $compileProvider.directive('formatter', function() {
return {
require: 'ngModel',
link: function (scope, elm, attrs, ngModelCtrl) {
- ngModelCtrl.$formatters.unshift(function (viewVal) {
+ ngModelCtrl.$formatters.unshift(function(viewVal) {
return 'formatted' + viewVal;
});
}
};
});
- $compileProvider.directive('childDirective', function () {
+ $compileProvider.directive('childDirective', function() {
return {
restrict: 'A',
require: '^parentDirective',
@@ -25,7 +25,7 @@ describe('typeahead tests', function () {
};
});
}));
- beforeEach(inject(function (_$rootScope_, _$compile_, _$document_, _$timeout_, $sniffer) {
+ beforeEach(inject(function(_$rootScope_, _$compile_, _$document_, _$templateCache_, _$timeout_, _$window_, $sniffer) {
$scope = _$rootScope_;
$scope.source = ['foo', 'bar', 'baz'];
$scope.states = [
@@ -34,8 +34,10 @@ describe('typeahead tests', function () {
];
$compile = _$compile_;
$document = _$document_;
+ $templateCache = _$templateCache_;
$timeout = _$timeout_;
- changeInputValueTo = function (element, value) {
+ $window = _$window_;
+ changeInputValueTo = function(element, value) {
var inputEl = findInput(element);
inputEl.val(value);
inputEl.trigger($sniffer.hasEvent('input') ? 'input' : 'change');
@@ -44,28 +46,32 @@ describe('typeahead tests', function () {
}));
//utility functions
- var prepareInputEl = function (inputTpl) {
+ var prepareInputEl = function(inputTpl) {
var el = $compile(angular.element(inputTpl))($scope);
$scope.$digest();
return el;
};
- var findInput = function (element) {
+ var findInput = function(element) {
return element.find('input');
};
- var findDropDown = function (element) {
+ var findDropDown = function(element) {
return element.find('ul.dropdown-menu');
};
- var findMatches = function (element) {
+ var findMatches = function(element) {
return findDropDown(element).find('li');
};
- var triggerKeyDown = function (element, keyCode) {
+ var triggerKeyDown = function(element, keyCode, options) {
+ options = options || {};
var inputEl = findInput(element);
var e = $.Event('keydown');
e.which = keyCode;
+ if (options.shiftKey) {
+ e.shiftKey = true;
+ }
inputEl.trigger(e);
};
@@ -117,40 +123,37 @@ describe('typeahead tests', function () {
});
});
- afterEach(function () {
+ afterEach(function() {
findDropDown($document.find('body')).remove();
});
//coarse grained, "integration" tests
- describe('initial state and model changes', function () {
-
- it('should be closed by default', function () {
- var element = prepareInputEl('');
+ describe('initial state and model changes', function() {
+ it('should be closed by default', function() {
+ var element = prepareInputEl('');
expect(element).toBeClosed();
});
- it('should correctly render initial state if the "as" keyword is used', function () {
-
+ it('should correctly render initial state if the "as" keyword is used', function() {
$scope.result = $scope.states[0];
- var element = prepareInputEl('');
+ var element = prepareInputEl('');
var inputEl = findInput(element);
expect(inputEl.val()).toEqual('Alaska');
});
- it('should default to bound model for initial rendering if there is not enough info to render label', function () {
-
+ it('should default to bound model for initial rendering if there is not enough info to render label', function() {
$scope.result = $scope.states[0].code;
- var element = prepareInputEl('');
+ var element = prepareInputEl('');
var inputEl = findInput(element);
expect(inputEl.val()).toEqual('AL');
});
- it('should not get open on model change', function () {
- var element = prepareInputEl('');
+ it('should not get open on model change', function() {
+ var element = prepareInputEl('');
$scope.$apply(function () {
$scope.result = 'foo';
});
@@ -158,10 +161,9 @@ describe('typeahead tests', function () {
});
});
- describe('basic functionality', function () {
-
- it('should open and close typeahead based on matches', function () {
- var element = prepareInputEl('');
+ describe('basic functionality', function() {
+ it('should open and close typeahead based on matches', function() {
+ var element = prepareInputEl('');
var inputEl = findInput(element);
var ownsId = inputEl.attr('aria-owns');
@@ -182,8 +184,8 @@ describe('typeahead tests', function () {
expect(inputEl.attr('aria-activedescendant')).toBeUndefined();
});
- it('should allow expressions over multiple lines', function () {
- var element = prepareInputEl('');
changeInputValueTo(element, 'ba');
expect(element).toBeOpenWithActive(2, 0);
@@ -192,50 +194,71 @@ describe('typeahead tests', function () {
expect(element).toBeClosed();
});
- it('should not open typeahead if input value smaller than a defined threshold', function () {
- var element = prepareInputEl('');
+ it('should not open typeahead if input value smaller than a defined threshold', function() {
+ var element = prepareInputEl('');
changeInputValueTo(element, 'b');
expect(element).toBeClosed();
});
- it('should support custom model selecting function', function () {
- $scope.updaterFn = function (selectedItem) {
+
+ it('should support changing min-length', function() {
+ $scope.typeAheadMinLength = 2;
+ var element = prepareInputEl('');
+
+ changeInputValueTo(element, 'b');
+
+ expect(element).toBeClosed();
+
+ $scope.typeAheadMinLength = 0;
+ $scope.$digest();
+ changeInputValueTo(element, '');
+
+ expect(element).toBeOpenWithActive(3, 0);
+
+ $scope.typeAheadMinLength = 2;
+ $scope.$digest();
+ changeInputValueTo(element, 'b');
+
+ expect(element).toBeClosed();
+ });
+
+ it('should support custom model selecting function', function() {
+ $scope.updaterFn = function(selectedItem) {
return 'prefix' + selectedItem;
};
- var element = prepareInputEl('');
+ var element = prepareInputEl('');
changeInputValueTo(element, 'f');
triggerKeyDown(element, 13);
expect($scope.result).toEqual('prefixfoo');
});
- it('should support custom label rendering function', function () {
- $scope.formatterFn = function (sourceItem) {
+ it('should support custom label rendering function', function() {
+ $scope.formatterFn = function(sourceItem) {
return 'prefix' + sourceItem;
};
- var element = prepareInputEl('');
+ var element = prepareInputEl('');
changeInputValueTo(element, 'fo');
var matchHighlight = findMatches(element).find('a').html();
expect(matchHighlight).toEqual('prefixfoo');
});
- it('should by default bind view value to model even if not part of matches', function () {
- var element = prepareInputEl('');
+ it('should by default bind view value to model even if not part of matches', function() {
+ var element = prepareInputEl('');
changeInputValueTo(element, 'not in matches');
expect($scope.result).toEqual('not in matches');
});
- it('should support the editable property to limit model bindings to matches only', function () {
- var element = prepareInputEl('');
+ it('should support the editable property to limit model bindings to matches only', function() {
+ var element = prepareInputEl('');
changeInputValueTo(element, 'not in matches');
expect($scope.result).toEqual(undefined);
});
- it('should set validation errors for non-editable inputs', function () {
-
+ it('should set validation errors for non-editable inputs', function() {
var element = prepareInputEl(
'');
changeInputValueTo(element, 'not in matches');
@@ -248,30 +271,147 @@ describe('typeahead tests', function () {
expect($scope.form.input.$error.editable).toBeFalsy();
});
- it('should not set editable validation error for empty input', function () {
+ it('should not set editable validation error for empty input', function() {
var element = prepareInputEl(
'');
changeInputValueTo(element, 'not in matches');
expect($scope.result).toEqual(undefined);
expect($scope.form.input.$error.editable).toBeTruthy();
changeInputValueTo(element, '');
- expect($scope.result).toEqual('');
+ expect($scope.result).toEqual(null);
expect($scope.form.input.$error.editable).toBeFalsy();
});
- it('should bind loading indicator expression', inject(function ($timeout) {
+ it('should clear view value after blur for typeahead-editable="false"', function () {
+ var element = prepareInputEl('');
+ var inputEl = findInput(element);
+
+ changeInputValueTo(element, 'not in matches');
+ expect($scope.result).toEqual(undefined);
+ expect(inputEl.val()).toEqual('not in matches');
+ inputEl.blur(); // input loses focus
+ expect($scope.result).toEqual(undefined);
+ expect(inputEl.val()).toEqual('');
+ });
+
+ it('should clear errors after blur for typeahead-editable="false"', function () {
+ var element = prepareInputEl(
+ '');
+ var inputEl = findInput(element);
+
+ changeInputValueTo(element, 'not in matches');
+ expect($scope.result).toEqual(undefined);
+ expect(inputEl.val()).toEqual('not in matches');
+ inputEl.blur();
+
+ expect($scope.form.input.$error.editable).toBeFalsy();
+ expect($scope.form.input.$error.parse).toBeFalsy();
+ });
+
+ // fix for #6032
+ it('should clear errors and refresh scope after blur for typeahead-editable="false"', function () {
+ var element = prepareInputEl(
+ '');
+ var inputEl = findInput(element);
+
+ // first try
+ changeInputValueTo(element, 'not in matches');
+ expect($scope.result).toEqual(undefined);
+ expect(inputEl.val()).toEqual('not in matches');
+ expect(element.find('form')).toHaveClass('invalid');
+ inputEl.blur();
+
+ expect(inputEl.val()).toEqual(''); // <-- input is reset
+ expect($scope.form.input.$error.editable).toBeFalsy();
+ expect($scope.form.input.$error.parse).toBeFalsy();
+ expect(element.find('form')).not.toHaveClass('invalid'); // <-- form has no error (it always works for some reason)
+
+ // second try
+ changeInputValueTo(element, 'not in matches');
+ expect($scope.result).toEqual(undefined);
+ expect(inputEl.val()).toEqual('not in matches');
+ expect(element.find('form')).toHaveClass('invalid');
+ inputEl.blur();
+
+ expect(inputEl.val()).toEqual(''); // <-- input is reset
+ expect($scope.form.input.$error.editable).toBeFalsy();
+ expect($scope.form.input.$error.parse).toBeFalsy();
+ expect(element.find('form')).not.toHaveClass('invalid'); // <-- form has no error (it didn't work prior to #6032 fix)
+ });
+ it('should go through other validators after blur for typeahead-editable="false"', function () {
+ var element = prepareInputEl(
+ '');
+ var inputEl = findInput(element);
+
+ changeInputValueTo(element, 'not in matches');
+ expect($scope.result).toEqual(undefined);
+ expect(inputEl.val()).toEqual('not in matches');
+ inputEl.blur(); // input loses focus
+ expect($scope.result).toEqual(undefined);
+ expect($scope.form.input.$error.required).toBeTruthy();
+ });
+
+ it('should clear view value when no value selected for typeahead-editable="false" typeahead-select-on-blur="false"', function () {
+ var element = prepareInputEl('');
+ var inputEl = findInput(element);
+
+ changeInputValueTo(element, 'b');
+ expect($scope.result).toEqual(undefined);
+ expect(inputEl.val()).toEqual('b');
+ inputEl.blur(); // input loses focus
+ expect($scope.result).toEqual(undefined);
+ expect(inputEl.val()).toEqual('');
+ });
+
+ it('should not clear view value when there is match but no value selected for typeahead-editable="false" typeahead-select-on-blur="true"', function () {
+ var element = prepareInputEl('');
+ var inputEl = findInput(element);
+
+ changeInputValueTo(element, 'b');
+ expect($scope.result).toEqual(undefined);
+ expect(inputEl.val()).toEqual('b');
+ inputEl.blur(); // input loses focus
+ expect($scope.result).toEqual('bar');
+ expect(inputEl.val()).toEqual('bar');
+ });
+
+ it('should support changing the editable property to limit model bindings to matches only', function() {
+ $scope.isEditable = true;
+ var element = prepareInputEl('');
+ $scope.isEditable = false;
+ $scope.$digest();
+ changeInputValueTo(element, 'not in matches');
+ expect($scope.result).toEqual(undefined);
+ });
+
+ it('should support changing the editable property to bind view value to model even if not part of matches', function() {
+ $scope.isEditable = false;
+ var element = prepareInputEl('');
+ $scope.isEditable = true;
+ $scope.$digest();
+ changeInputValueTo(element, 'not in matches');
+ expect($scope.result).toEqual('not in matches');
+ });
+
+ it('should bind loading indicator expression', inject(function($timeout) {
$scope.isLoading = false;
- $scope.loadMatches = function (viewValue) {
- return $timeout(function () {
+ $scope.loadMatches = function(viewValue) {
+ return $timeout(function() {
return [];
}, 1000);
};
- var element = prepareInputEl('');
+ var element = prepareInputEl('');
changeInputValueTo(element, 'foo');
expect($scope.isLoading).toBeTruthy();
@@ -279,9 +419,8 @@ describe('typeahead tests', function () {
expect($scope.isLoading).toBeFalsy();
}));
- it('should support timeout before trying to match $viewValue', inject(function ($timeout) {
-
- var element = prepareInputEl('');
+ it('should support timeout before trying to match $viewValue', inject(function($timeout) {
+ var element = prepareInputEl('');
changeInputValueTo(element, 'foo');
expect(element).toBeClosed();
@@ -289,13 +428,13 @@ describe('typeahead tests', function () {
expect(element).toBeOpenWithActive(1, 0);
}));
- it('should cancel old timeouts when something is typed within waitTime', inject(function ($timeout) {
+ it('should cancel old timeouts when something is typed within waitTime', inject(function($timeout) {
var values = [];
$scope.loadMatches = function(viewValue) {
values.push(viewValue);
return $scope.source;
};
- var element = prepareInputEl('');
+ var element = prepareInputEl('');
changeInputValueTo(element, 'first');
changeInputValueTo(element, 'second');
@@ -304,14 +443,14 @@ describe('typeahead tests', function () {
expect(values).not.toContain('first');
}));
- it('should allow timeouts when something is typed after waitTime has passed', inject(function ($timeout) {
+ it('should allow timeouts when something is typed after waitTime has passed', inject(function($timeout) {
var values = [];
$scope.loadMatches = function(viewValue) {
values.push(viewValue);
return $scope.source;
};
- var element = prepareInputEl('');
+ var element = prepareInputEl('');
changeInputValueTo(element, 'first');
$timeout.flush();
@@ -324,43 +463,61 @@ describe('typeahead tests', function () {
expect(values).toContain('second');
}));
- it('should support custom templates for matched items', inject(function ($templateCache) {
+ it('should support custom popup templates', function() {
+ $templateCache.put('custom.html', '
foo
');
+
+ var element = prepareInputEl('');
+ changeInputValueTo(element, 'Al');
+
+ expect(element.find('.custom').text()).toBe('foo');
+ });
+
+ it('should support custom templates for matched items', function() {
$templateCache.put('custom.html', '
{{ index }} {{ match.label }}
');
- var element = prepareInputEl('');
+ var element = prepareInputEl('');
changeInputValueTo(element, 'Al');
expect(findMatches(element).eq(0).find('p').text()).toEqual('0 Alaska');
- }));
-
- it('should support directives which require controllers in custom templates for matched items', inject(function ($templateCache) {
+ });
+ it('should support directives which require controllers in custom templates for matched items', function() {
$templateCache.put('custom.html', '
{{ index }} {{ match.label }}
');
- var element = prepareInputEl('');
+ var element = prepareInputEl('');
element.data('$parentDirectiveController', {});
changeInputValueTo(element, 'Al');
expect(findMatches(element).eq(0).find('p').text()).toEqual('0 Alaska');
- }));
+ });
- it('should throw error on invalid expression', function () {
- var prepareInvalidDir = function () {
- prepareInputEl('');
+ it('should throw error on invalid expression', function() {
+ var prepareInvalidDir = function() {
+ prepareInputEl('');
};
expect(prepareInvalidDir).toThrow();
});
- });
- describe('selecting a match', function () {
+ it('should remove the id attribute from the original DOM element', function() {
+ var element = prepareInputEl('');
+ var inputEl = findInput(element);
- it('should select a match on enter', function () {
+ expect(inputEl.size()).toBe(2);
+ expect(inputEl.eq(0).attr('id')).toBe(undefined);
+ expect(inputEl.eq(1).attr('id')).toBe('typeahead-element');
+ });
+ });
- var element = prepareInputEl('');
+ describe('shouldSelect', function() {
+ it('should select a match when function returns true', function() {
+ $scope.shouldSelectFn = function() {
+ return true;
+ };
+ var element = prepareInputEl('');
var inputEl = findInput(element);
changeInputValueTo(element, 'b');
@@ -370,10 +527,48 @@ describe('typeahead tests', function () {
expect(inputEl.val()).toEqual('bar');
expect(element).toBeClosed();
});
+ it('should not select a match when function returns false', function() {
+ $scope.shouldSelectFn = function() {
+ return false;
+ };
+ var element = prepareInputEl('');
+ var inputEl = findInput(element);
+
+ changeInputValueTo(element, 'b');
+ triggerKeyDown(element, 13);
+
+ // no change
+ expect($scope.result).toEqual('b');
+ expect(inputEl.val()).toEqual('b');
+ });
+ it('should pass key event into select trigger function', function() {
+ $scope.shouldSelectFn = jasmine.createSpy('shouldSelectFn');//.and.returnValue(true);
+ var element = prepareInputEl('');
+ var inputEl = findInput(element);
+
+ changeInputValueTo(element, 'b');
+ triggerKeyDown(element, 13);
+
+ expect($scope.shouldSelectFn.calls.count()).toEqual(1);
+ expect($scope.shouldSelectFn.calls.argsFor(0)[0].which).toEqual(13);
+ });
+ });
- it('should select a match on tab', function () {
+ describe('selecting a match', function() {
+ it('should select a match on enter', function() {
+ var element = prepareInputEl('');
+ var inputEl = findInput(element);
+
+ changeInputValueTo(element, 'b');
+ triggerKeyDown(element, 13);
+
+ expect($scope.result).toEqual('bar');
+ expect(inputEl.val()).toEqual('bar');
+ expect(element).toBeClosed();
+ });
- var element = prepareInputEl('');
+ it('should select a match on tab', function() {
+ var element = prepareInputEl('');
var inputEl = findInput(element);
changeInputValueTo(element, 'b');
@@ -384,9 +579,32 @@ describe('typeahead tests', function () {
expect(element).toBeClosed();
});
- it('should select match on click', function () {
+ it('should not select any match on blur without \'select-on-blur=true\' option', function() {
+ var element = prepareInputEl('');
+ var inputEl = findInput(element);
- var element = prepareInputEl('');
+ changeInputValueTo(element, 'b');
+ inputEl.blur(); // input loses focus
+
+ // no change
+ expect($scope.result).toEqual('b');
+ expect(inputEl.val()).toEqual('b');
+ });
+
+ it('should select a match on blur with \'select-on-blur=true\' option', function() {
+ var element = prepareInputEl('');
+ var inputEl = findInput(element);
+
+ changeInputValueTo(element, 'b');
+ inputEl.blur(); // input loses focus
+
+ // first element should be selected
+ expect($scope.result).toEqual('bar');
+ expect(inputEl.val()).toEqual('bar');
+ });
+
+ it('should select match on click', function() {
+ var element = prepareInputEl('');
var inputEl = findInput(element);
changeInputValueTo(element, 'b');
@@ -400,14 +618,14 @@ describe('typeahead tests', function () {
expect(element).toBeClosed();
});
- it('should invoke select callback on select', function () {
-
- $scope.onSelect = function ($item, $model, $label) {
+ it('should invoke select callback on select', function() {
+ $scope.onSelect = function($item, $model, $label, $event) {
$scope.$item = $item;
$scope.$model = $model;
$scope.$label = $label;
+ $scope.$event = $event;
};
- var element = prepareInputEl('');
+ var element = prepareInputEl('');
changeInputValueTo(element, 'Alas');
triggerKeyDown(element, 13);
@@ -416,11 +634,11 @@ describe('typeahead tests', function () {
expect($scope.$item).toEqual($scope.states[0]);
expect($scope.$model).toEqual('AL');
expect($scope.$label).toEqual('Alaska');
+ expect($scope.$event.type).toEqual("keydown");
});
- it('should correctly update inputs value on mapping where label is not derived from the model', function () {
-
- var element = prepareInputEl('');
+ it('should correctly update inputs value on mapping where label is not derived from the model', function() {
+ var element = prepareInputEl('');
var inputEl = findInput(element);
changeInputValueTo(element, 'Alas');
@@ -429,37 +647,222 @@ describe('typeahead tests', function () {
expect($scope.result).toEqual('AL');
expect(inputEl.val()).toEqual('AL');
});
+
+ it('should bind no results indicator as true when no matches returned', inject(function($timeout) {
+ $scope.isNoResults = false;
+ $scope.loadMatches = function(viewValue) {
+ return $timeout(function() {
+ return [];
+ }, 1000);
+ };
+
+ var element = prepareInputEl('');
+ changeInputValueTo(element, 'foo');
+
+ expect($scope.isNoResults).toBeFalsy();
+ $timeout.flush();
+ expect($scope.isNoResults).toBeTruthy();
+ }));
+
+ it('should bind no results indicator as false when matches are returned', inject(function($timeout) {
+ $scope.isNoResults = false;
+ $scope.loadMatches = function(viewValue) {
+ return $timeout(function() {
+ return [viewValue];
+ }, 1000);
+ };
+
+ var element = prepareInputEl('');
+ changeInputValueTo(element, 'foo');
+
+ expect($scope.isNoResults).toBeFalsy();
+ $timeout.flush();
+ expect($scope.isNoResults).toBeFalsy();
+ }));
+
+ it('should not focus the input if `typeahead-focus-on-select` is false', function() {
+ var element = prepareInputEl('');
+ $document.find('body').append(element);
+ var inputEl = findInput(element);
+
+ changeInputValueTo(element, 'b');
+ var match = $(findMatches(element)[1]).find('a')[0];
+
+ $(match).click();
+ $scope.$digest();
+ $timeout.flush();
+
+ expect(document.activeElement).not.toBe(inputEl[0]);
+ expect($scope.result).toEqual('baz');
+ });
+ });
+
+ describe('select on exact match', function() {
+ it('should select on an exact match when set', function() {
+ $scope.onSelect = jasmine.createSpy('onSelect');
+ var element = prepareInputEl('');
+ var inputEl = findInput(element);
+
+ changeInputValueTo(element, 'bar');
+
+ expect($scope.result).toEqual('bar');
+ expect(inputEl.val()).toEqual('bar');
+ expect(element).toBeClosed();
+ expect($scope.onSelect).toHaveBeenCalled();
+ });
+
+ it('should not select on an exact match by default', function() {
+ $scope.onSelect = jasmine.createSpy('onSelect');
+ var element = prepareInputEl('');
+ var inputEl = findInput(element);
+
+ changeInputValueTo(element, 'bar');
+
+ expect($scope.result).toBeUndefined();
+ expect(inputEl.val()).toEqual('bar');
+ expect($scope.onSelect.calls.any()).toBe(false);
+ });
+
+ it('should not be case sensitive when select on an exact match', function() {
+ $scope.onSelect = jasmine.createSpy('onSelect');
+ var element = prepareInputEl('');
+ var inputEl = findInput(element);
+
+ changeInputValueTo(element, 'BaR');
+
+ expect($scope.result).toEqual('bar');
+ expect(inputEl.val()).toEqual('bar');
+ expect(element).toBeClosed();
+ expect($scope.onSelect).toHaveBeenCalled();
+ });
+
+ it('should not auto select when not a match with one potential result left', function() {
+ $scope.onSelect = jasmine.createSpy('onSelect');
+ var element = prepareInputEl('');
+ var inputEl = findInput(element);
+
+ changeInputValueTo(element, 'fo');
+
+ expect($scope.result).toBeUndefined();
+ expect(inputEl.val()).toEqual('fo');
+ expect($scope.onSelect.calls.any()).toBe(false);
+ });
});
- describe('pop-up interaction', function () {
+ describe('is-open indicator', function () {
+ var element;
+
+ beforeEach(function () {
+ element = prepareInputEl('');
+ });
+
+ it('should bind is-open indicator as true when matches are returned', function () {
+ expect($scope.isOpen).toBeFalsy();
+ changeInputValueTo(element, 'b');
+ expect($scope.isOpen).toBeTruthy();
+ });
+
+ it('should bind is-open indicator as false when no matches returned', function () {
+ expect($scope.isOpen).toBeFalsy();
+ changeInputValueTo(element, 'b');
+ expect($scope.isOpen).toBeTruthy();
+ changeInputValueTo(element, 'not match');
+ expect($scope.isOpen).toBeFalsy();
+ });
+
+ it('should bind is-open indicator as false when a match is clicked', function () {
+ expect($scope.isOpen).toBeFalsy();
+ changeInputValueTo(element, 'b');
+ expect($scope.isOpen).toBeTruthy();
+ var match = findMatches(element).find('a').eq(0);
+
+ match.click();
+ $scope.$digest();
+ expect($scope.isOpen).toBeFalsy();
+ });
+ it('should bind is-open indicator as false when click outside', function () {
+ expect($scope.isOpen).toBeFalsy();
+ changeInputValueTo(element, 'b');
+ expect($scope.isOpen).toBeTruthy();
+ $document.find('body').click();
+ $scope.$digest();
+ expect($scope.isOpen).toBeFalsy();
+ });
+
+ it('should bind is-open indicator as false on enter', function () {
+ expect($scope.isOpen).toBeFalsy();
+ changeInputValueTo(element, 'b');
+ expect($scope.isOpen).toBeTruthy();
+ triggerKeyDown(element, 13);
+ expect($scope.isOpen).toBeFalsy();
+ });
+
+ it('should bind is-open indicator as false on tab', function () {
+ expect($scope.isOpen).toBeFalsy();
+ changeInputValueTo(element, 'b');
+ expect($scope.isOpen).toBeTruthy();
+ triggerKeyDown(element, 9);
+ expect($scope.isOpen).toBeFalsy();
+ });
+
+ it('should bind is-open indicator as false on escape key', function () {
+ expect($scope.isOpen).toBeFalsy();
+ changeInputValueTo(element, 'b');
+ expect($scope.isOpen).toBeTruthy();
+ triggerKeyDown(element, 27);
+ expect($scope.isOpen).toBeFalsy();
+ });
+
+ it('should bind is-open indicator as false input value smaller than a defined threshold', function () {
+ var element = prepareInputEl('');
+ expect($scope.isToggled).toBeFalsy();
+ changeInputValueTo(element, 'b');
+ expect($scope.isToggled).toBeFalsy();
+ });
+ });
+
+ describe('pop-up interaction', function() {
var element;
- beforeEach(function () {
- element = prepareInputEl('');
+ beforeEach(function() {
+ element = prepareInputEl('');
});
- it('should activate prev/next matches on up/down keys', function () {
+ it('should activate prev/next matches on up/down keys', function() {
changeInputValueTo(element, 'b');
- expect(element).toBeOpenWithActive(2, 0);
+ var parentNode = element.find('ul').eq(0)[0];
+ var liIndex;
+
+ liIndex = 0;
+ expect(element).toBeOpenWithActive(2, liIndex);
+ expect(parentNode.scrollTop).toEqual(element.find('li').eq(liIndex)[0].offsetTop);
// Down arrow key
triggerKeyDown(element, 40);
- expect(element).toBeOpenWithActive(2, 1);
+ liIndex = 1;
+ expect(element).toBeOpenWithActive(2, liIndex);
+ expect(parentNode.scrollTop).toEqual(element.find('li').eq(liIndex)[0].offsetTop);
// Down arrow key goes back to first element
triggerKeyDown(element, 40);
- expect(element).toBeOpenWithActive(2, 0);
+ liIndex = 0;
+ expect(element).toBeOpenWithActive(2, liIndex);
+ expect(parentNode.scrollTop).toEqual(element.find('li').eq(liIndex)[0].offsetTop);
// Up arrow key goes back to last element
triggerKeyDown(element, 38);
- expect(element).toBeOpenWithActive(2, 1);
+ liIndex = 1;
+ expect(element).toBeOpenWithActive(2, liIndex);
+ expect(parentNode.scrollTop).toEqual(element.find('li').eq(liIndex)[0].offsetTop);
// Up arrow key goes back to first element
triggerKeyDown(element, 38);
- expect(element).toBeOpenWithActive(2, 0);
+ liIndex = 0;
+ expect(parentNode.scrollTop).toEqual(element.find('li').eq(liIndex)[0].offsetTop);
+ expect(element).toBeOpenWithActive(2, liIndex);
});
- it('should close popup on escape key', function () {
+ it('should close popup on escape key', function() {
changeInputValueTo(element, 'b');
expect(element).toBeOpenWithActive(2, 0);
@@ -468,28 +871,27 @@ describe('typeahead tests', function () {
expect(element).toBeClosed();
});
- it('should highlight match on mouseenter', function () {
+ it('should highlight match on mouseenter', function() {
changeInputValueTo(element, 'b');
expect(element).toBeOpenWithActive(2, 0);
findMatches(element).eq(1).trigger('mouseenter');
expect(element).toBeOpenWithActive(2, 1);
});
-
});
- describe('promises', function () {
+ describe('promises', function() {
var element, deferred;
- beforeEach(inject(function ($q) {
+ beforeEach(inject(function($q) {
deferred = $q.defer();
- $scope.source = function () {
+ $scope.source = function() {
return deferred.promise;
};
- element = prepareInputEl('');
+ element = prepareInputEl('');
}));
- it('should display matches from promise', function () {
+ it('should display matches from promise', function() {
changeInputValueTo(element, 'c');
expect(element).toBeClosed();
@@ -498,7 +900,7 @@ describe('typeahead tests', function () {
expect(element).toBeOpenWithActive(2, 0);
});
- it('should not display anything when promise is rejected', function () {
+ it('should not display anything when promise is rejected', function() {
changeInputValueTo(element, 'c');
expect(element).toBeClosed();
@@ -507,7 +909,7 @@ describe('typeahead tests', function () {
expect(element).toBeClosed();
});
- it('PR #3178, resolves #2999 - should not return property "length" of undefined for undefined matches', function () {
+ it('PR #3178, resolves #2999 - should not return property "length" of undefined for undefined matches', function() {
changeInputValueTo(element, 'c');
expect(element).toBeClosed();
@@ -515,13 +917,12 @@ describe('typeahead tests', function () {
$scope.$digest();
expect(element).toBeClosed();
});
-
});
- describe('non-regressions tests', function () {
+ describe('non-regressions tests', function() {
- it('issue 231 - closes matches popup on click outside typeahead', function () {
- var element = prepareInputEl('');
+ it('issue 231 - closes matches popup on click outside typeahead', function() {
+ var element = prepareInputEl('');
changeInputValueTo(element, 'b');
@@ -531,24 +932,22 @@ describe('typeahead tests', function () {
expect(element).toBeClosed();
});
- it('issue 591 - initial formatting for un-selected match and complex label expression', function () {
-
- var inputEl = findInput(prepareInputEl(''));
+ it('issue 591 - initial formatting for un-selected match and complex label expression', function() {
+ var inputEl = findInput(prepareInputEl(''));
expect(inputEl.val()).toEqual('');
});
- it('issue 786 - name of internal model should not conflict with scope model name', function () {
+ it('issue 786 - name of internal model should not conflict with scope model name', function() {
$scope.state = $scope.states[0];
- var element = prepareInputEl('');
+ var element = prepareInputEl('');
var inputEl = findInput(element);
expect(inputEl.val()).toEqual('Alaska');
});
- it('issue 863 - it should work correctly with input type="email"', function () {
-
+ it('issue 863 - it should work correctly with input type="email"', function() {
$scope.emails = ['foo@host.com', 'bar@host.com'];
- var element = prepareInputEl('');
+ var element = prepareInputEl('');
var inputEl = findInput(element);
changeInputValueTo(element, 'bar');
@@ -560,14 +959,13 @@ describe('typeahead tests', function () {
expect(inputEl.val()).toEqual('bar@host.com');
});
- it('issue 964 - should not show popup with matches if an element is not focused', function () {
-
+ it('issue 964 - should not show popup with matches if an element is not focused', function() {
$scope.items = function(viewValue) {
- return $timeout(function(){
+ return $timeout(function() {
return [viewValue];
});
};
- var element = prepareInputEl('');
+ var element = prepareInputEl('');
var inputEl = findInput(element);
changeInputValueTo(element, 'match');
@@ -579,14 +977,13 @@ describe('typeahead tests', function () {
expect(element).toBeClosed();
});
- it('should properly update loading callback if an element is not focused', function () {
-
+ it('should properly update loading callback if an element is not focused', function() {
$scope.items = function(viewValue) {
return $timeout(function(){
return [viewValue];
});
};
- var element = prepareInputEl('');
+ var element = prepareInputEl('');
var inputEl = findInput(element);
changeInputValueTo(element, 'match');
@@ -598,14 +995,13 @@ describe('typeahead tests', function () {
expect($scope.isLoading).toBeFalsy();
});
- it('issue 1140 - should properly update loading callback when deleting characters', function () {
-
+ it('issue 1140 - should properly update loading callback when deleting characters', function() {
$scope.items = function(viewValue) {
- return $timeout(function(){
+ return $timeout(function() {
return [viewValue];
});
};
- var element = prepareInputEl('');
+ var element = prepareInputEl('');
changeInputValueTo(element, 'match');
$scope.$digest();
@@ -619,13 +1015,13 @@ describe('typeahead tests', function () {
expect($scope.isLoading).toBeFalsy();
});
- it('should cancel old timeout when deleting characters', inject(function ($timeout) {
+ it('should cancel old timeout when deleting characters', inject(function($timeout) {
var values = [];
$scope.loadMatches = function(viewValue) {
values.push(viewValue);
return $scope.source;
};
- var element = prepareInputEl('');
+ var element = prepareInputEl('');
changeInputValueTo(element, 'match');
changeInputValueTo(element, 'm');
@@ -638,8 +1034,8 @@ describe('typeahead tests', function () {
// Dummy describe to be able to create an after hook for this tests
var element;
- it('does not close matches popup on click in input', function () {
- element = prepareInputEl('');
+ it('does not close matches popup on click in input', function() {
+ element = prepareInputEl('');
var inputEl = findInput(element);
// Note that this bug can only be found when element is in the document
@@ -653,8 +1049,8 @@ describe('typeahead tests', function () {
expect(element).toBeOpenWithActive(2, 0);
});
- it('issue #1773 - should not trigger an error when used with ng-focus', function () {
- element = prepareInputEl('');
+ it('issue #1773 - should not trigger an error when used with ng-focus', function() {
+ element = prepareInputEl('');
var inputEl = findInput(element);
// Note that this bug can only be found when element is in the document
@@ -672,27 +1068,25 @@ describe('typeahead tests', function () {
});
});
- it('issue #1238 - allow names like "query" to be used inside "in" expressions ', function () {
-
+ it('issue #1238 - allow names like "query" to be used inside "in" expressions ', function() {
$scope.query = function() {
return ['foo', 'bar'];
};
- var element = prepareInputEl('');
+ var element = prepareInputEl('');
changeInputValueTo(element, 'bar');
expect(element).toBeOpenWithActive(2, 0);
});
- it('issue #3318 - should set model validity to true when set manually', function () {
-
+ it('issue #3318 - should set model validity to true when set manually', function() {
var element = prepareInputEl(
'');
changeInputValueTo(element, 'not in matches');
- $scope.$apply(function () {
+ $scope.$apply(function() {
$scope.result = 'manually set';
});
@@ -700,8 +1094,8 @@ describe('typeahead tests', function () {
expect($scope.form.input.$valid).toBeTruthy();
});
- it('issue #3166 - should set \'parse\' key as valid when selecting a perfect match and not editable', function () {
- var element = prepareInputEl('');
+ it('issue #3166 - should set \'parse\' key as valid when selecting a perfect match and not editable', function() {
+ var element = prepareInputEl('');
var inputEl = findInput(element);
changeInputValueTo(element, 'Alaska');
@@ -711,52 +1105,293 @@ describe('typeahead tests', function () {
});
});
- describe('input formatting', function () {
+ describe('ng-model-options', function() {
+ it('should support getterSetter', function() {
+ function resultSetter(state) {
+ return state;
+ }
+ $scope.result = resultSetter;
+ var element = prepareInputEl('');
- it('should co-operate with existing formatters', function () {
+ changeInputValueTo(element, 'Alaska');
+ triggerKeyDown(element, 13);
+
+ expect($scope.result).toBe(resultSetter);
+ });
+
+ describe('debounce as a number', function() {
+ it('should work with selecting via keyboard', function() {
+ element = prepareInputEl('');
+ var inputEl = findInput(element);
+
+ changeInputValueTo(element, 'Alaska');
+ triggerKeyDown(element, 13);
+
+ expect($scope.result).not.toBe('Alaska');
+
+ $timeout.flush(400);
+
+ expect($scope.result).toBe('Alaska');
+ });
+
+ it('should work with select on exact', function() {
+ element = prepareInputEl('');
+ var inputEl = findInput(element);
+
+ changeInputValueTo(element, 'Alaska');
+
+ expect($scope.result).not.toBe('Alaska');
+
+ $timeout.flush(400);
+
+ expect($scope.result).toBe('Alaska');
+ });
+ it('should work with selecting a match via click', function() {
+ element = prepareInputEl('');
+ var inputEl = findInput(element);
+
+ changeInputValueTo(element, 'Alaska');
+ var match = $(findMatches(element)[0]).find('a')[0];
+
+ $(match).click();
+ $scope.$digest();
+
+ expect($scope.result).not.toBe('Alaska');
+
+ $timeout.flush(400);
+
+ expect($scope.result).toBe('Alaska');
+ });
+ });
+
+ describe('debounce as an object', function() {
+ it('should work with selecting via keyboard', function() {
+ element = prepareInputEl('');
+ var inputEl = findInput(element);
+
+ changeInputValueTo(element, 'Alaska');
+ triggerKeyDown(element, 13);
+
+ expect($scope.result).not.toBe('Alaska');
+
+ $timeout.flush(400);
+
+ expect($scope.result).toBe('Alaska');
+ });
+
+ it('should work with select on exact', function() {
+ element = prepareInputEl('');
+ var inputEl = findInput(element);
+
+ changeInputValueTo(element, 'Alaska');
+
+ expect($scope.result).not.toBe('Alaska');
+
+ $timeout.flush(400);
+
+ expect($scope.result).toBe('Alaska');
+ });
+
+ it('should work with selecting a match via click', function() {
+ element = prepareInputEl('');
+ var inputEl = findInput(element);
+
+ changeInputValueTo(element, 'Alaska');
+ var match = $(findMatches(element)[0]).find('a')[0];
+
+ $(match).click();
+ $scope.$digest();
+
+ expect($scope.result).not.toBe('Alaska');
+
+ $timeout.flush(400);
+
+ expect($scope.result).toBe('Alaska');
+ });
+
+ it('should work when blurring and select on blur', function() {
+ element = prepareInputEl('');
+ var inputEl = findInput(element);
+
+ changeInputValueTo(element, 'Alaska');
+ element.blur();
+ $scope.$digest();
+
+ expect($scope.result).not.toBe('Alaska');
+
+ $timeout.flush(500);
+
+ expect($scope.result).toBe('Alaska');
+ });
+ });
+ });
+
+ describe('input formatting', function() {
+ it('should co-operate with existing formatters', function() {
$scope.result = $scope.states[0];
- var element = prepareInputEl(''),
+ var element = prepareInputEl(''),
inputEl = findInput(element);
expect(inputEl.val()).toEqual('formatted' + $scope.result.name);
});
- it('should support a custom input formatting function', function () {
-
+ it('should support a custom input formatting function', function() {
$scope.result = $scope.states[0];
$scope.formatInput = function($model) {
return $model.code;
};
- var element = prepareInputEl(''),
+ var element = prepareInputEl(''),
inputEl = findInput(element);
expect(inputEl.val()).toEqual('AL');
expect($scope.result).toEqual($scope.states[0]);
});
+ });
+ describe('input hint', function() {
+ var element;
+
+ beforeEach(function() {
+ element = prepareInputEl('');
+ });
+ it('should show hint when input matches first match', function() {
+ var hintEl = findInput(element);
+
+ expect(hintEl.val()).toEqual('');
+ changeInputValueTo(element, 'Alas');
+ expect(hintEl.val()).toEqual('Alaska');
+ });
+
+ it('should not show hint when input does not match first match', function() {
+ var hintEl = findInput(element);
+
+ expect(hintEl.val()).toEqual('');
+ changeInputValueTo(element, 'las');
+ expect(hintEl.val()).toEqual('');
+ });
+
+ it('should reset hint when a match is clicked', function() {
+ var hintEl = findInput(element);
+
+ expect(hintEl.val()).toEqual('');
+ changeInputValueTo(element, 'Alas');
+ expect(hintEl.val()).toEqual('Alaska');
+
+ var match = findMatches(element).find('a').eq(0);
+ match.click();
+ $scope.$digest();
+ expect(hintEl.val()).toEqual('');
+ });
+
+ it('should reset hint when click outside', function() {
+ var hintEl = findInput(element);
+
+ expect(hintEl.val()).toEqual('');
+ changeInputValueTo(element, 'Alas');
+ expect(hintEl.val()).toEqual('Alaska');
+
+ $document.find('body').click();
+ $scope.$digest();
+ expect(hintEl.val()).toEqual('');
+ });
+
+ it('should reset hint on enter', function() {
+ var hintEl = findInput(element);
+
+ expect(hintEl.val()).toEqual('');
+ changeInputValueTo(element, 'Alas');
+ expect(hintEl.val()).toEqual('Alaska');
+ triggerKeyDown(element, 13);
+ expect(hintEl.val()).toEqual('');
+ });
+
+ it('should reset hint on tab', function() {
+ var hintEl = findInput(element);
+
+ expect(hintEl.val()).toEqual('');
+ changeInputValueTo(element, 'Alas');
+ expect(hintEl.val()).toEqual('Alaska');
+ triggerKeyDown(element, 9);
+ expect(hintEl.val()).toEqual('');
+ });
+
+ it('should reset hint on escape key', function() {
+ var hintEl = findInput(element);
+
+ expect(hintEl.val()).toEqual('');
+ changeInputValueTo(element, 'Alas');
+ expect(hintEl.val()).toEqual('Alaska');
+ triggerKeyDown(element, 27);
+ expect(hintEl.val()).toEqual('');
+ });
+
+ it("should set tab index on hint input element", function(){
+ var hintEl = findInput(element);
+ expect(hintEl.attr('tabindex')).toEqual('-1');
+ });
});
- describe('append to body', function () {
- it('append typeahead results to body', function () {
- var element = prepareInputEl('');
+ describe('append to', function() {
+ it('append typeahead results to element', function() {
+ $document.find('body').append('');
+ $scope.myElement = $document.find('#myElement');
+ var element = prepareInputEl('');
+ changeInputValueTo(element, 'al');
+ expect($document.find('#myElement')).toBeOpenWithActive(2, 0);
+ $document.find('#myElement').remove();
+ });
+ });
+
+ describe('append to body', function() {
+ afterEach(function() {
+ angular.element($window).off('resize');
+ $document.find('body').off('scroll');
+ });
+
+ it('append typeahead results to body', function() {
+ var element = prepareInputEl('');
changeInputValueTo(element, 'ba');
expect($document.find('body')).toBeOpenWithActive(2, 0);
});
- it('should not append to body when value of the attribute is false', function () {
- var element = prepareInputEl('');
+ it('should not append to body when value of the attribute is false', function() {
+ var element = prepareInputEl('');
changeInputValueTo(element, 'ba');
expect(findDropDown($document.find('body')).length).toEqual(0);
});
+
+ it('should have right position after scroll', function() {
+ var element = prepareInputEl('');
+ var dropdown = findDropDown($document.find('body'));
+ var body = angular.element(document.body);
+
+ // Set body height to allow scrolling
+ body.css({height:'10000px'});
+
+ // Scroll top
+ window.scroll(0, 1000);
+
+ // Set input value to show dropdown
+ changeInputValueTo(element, 'ba');
+
+ // Init position of dropdown must be 1000px
+ expect(dropdown.css('top') ).toEqual('1000px');
+
+ // After scroll, must have new position
+ window.scroll(0, 500);
+ body.triggerHandler('scroll');
+ $timeout.flush();
+ expect(dropdown.css('top')).toEqual('500px');
+ });
});
- describe('focus first', function () {
- it('should focus the first element by default', function () {
- var element = prepareInputEl('');
+ describe('focus first', function() {
+ it('should focus the first element by default', function() {
+ var element = prepareInputEl('');
changeInputValueTo(element, 'b');
expect(element).toBeOpenWithActive(2, 0);
@@ -777,8 +1412,8 @@ describe('typeahead tests', function () {
expect(element).toBeOpenWithActive(2, 0);
});
- it('should not focus the first element until keys are pressed', function () {
- var element = prepareInputEl('');
+ it('should not focus the first element until keys are pressed', function() {
+ var element = prepareInputEl('');
changeInputValueTo(element, 'b');
expect(element).toBeOpenWithActive(2, -1);
@@ -813,12 +1448,12 @@ describe('typeahead tests', function () {
});
});
- it('should not capture enter or tab until an item is focused', function () {
+ it('should not capture enter or tab when an item is not focused', function() {
$scope.select_count = 0;
- $scope.onSelect = function ($item, $model, $label) {
+ $scope.onSelect = function($item, $model, $label) {
$scope.select_count = $scope.select_count + 1;
};
- var element = prepareInputEl('');
+ var element = prepareInputEl('');
changeInputValueTo(element, 'b');
// enter key should not be captured when nothing is focused
@@ -826,10 +1461,37 @@ describe('typeahead tests', function () {
expect($scope.keyDownEvent.isDefaultPrevented()).toBeFalsy();
expect($scope.select_count).toEqual(0);
- // tab key should not be captured when nothing is focused
+ // tab key should close the dropdown when nothing is focused
triggerKeyDown(element, 9);
expect($scope.keyDownEvent.isDefaultPrevented()).toBeFalsy();
expect($scope.select_count).toEqual(0);
+ expect(element).toBeClosed();
+ });
+
+ it("should not capture tab when shift key is pressed", function(){
+ $scope.select_count = 0;
+ $scope.onSelect = function($item, $model, $label) {
+ $scope.select_count = $scope.select_count + 1;
+ };
+ var element = prepareInputEl('');
+ changeInputValueTo(element, 'b');
+
+ // down key should be captured and focus first element
+ triggerKeyDown(element, 40);
+
+ triggerKeyDown(element, 9, {shiftKey: true});
+ expect($scope.keyDownEvent.isDefaultPrevented()).toBeFalsy();
+ expect($scope.select_count).toEqual(0);
+ expect(element).toBeClosed();
+ });
+
+ it('should capture enter or tab when an item is focused', function() {
+ $scope.select_count = 0;
+ $scope.onSelect = function($item, $model, $label) {
+ $scope.select_count = $scope.select_count + 1;
+ };
+ var element = prepareInputEl('');
+ changeInputValueTo(element, 'b');
// down key should be captured and focus first element
triggerKeyDown(element, 40);
@@ -842,4 +1504,94 @@ describe('typeahead tests', function () {
expect($scope.select_count).toEqual(1);
});
+ describe('minLength set to 0', function() {
+ it('should open typeahead if input is changed to empty string if defined threshold is 0', function() {
+ var element = prepareInputEl('');
+ changeInputValueTo(element, '');
+ expect(element).toBeOpenWithActive(3, 0);
+ });
+
+ it('should open typeahead when input is focused and value is empty if defined threshold is 0', function () {
+ var element = prepareInputEl('');
+ var inputEl = findInput(element);
+ inputEl.focus();
+ $timeout.flush();
+ $scope.$digest();
+ expect(element).toBeOpenWithActive(3, 0);
+ });
+ });
+
+ describe('event listeners', function() {
+ afterEach(function() {
+ angular.element($window).off('resize');
+ $document.find('body').off('scroll');
+ });
+
+ it('should register event listeners when attached to body', function() {
+ spyOn(window, 'addEventListener');
+ spyOn(document.body, 'addEventListener');
+
+ var element = prepareInputEl('');
+
+ expect(window.addEventListener).toHaveBeenCalledWith('resize', jasmine.any(Function), false);
+ expect(document.body.addEventListener).toHaveBeenCalledWith('scroll', jasmine.any(Function), false);
+ });
+
+ it('should remove event listeners when attached to body', function() {
+ spyOn(window, 'removeEventListener');
+ spyOn(document.body, 'removeEventListener');
+
+ var element = prepareInputEl('');
+ $scope.$destroy();
+
+ expect(window.removeEventListener).toHaveBeenCalledWith('resize', jasmine.any(Function), false);
+ expect(document.body.removeEventListener).toHaveBeenCalledWith('scroll', jasmine.any(Function), false);
+ });
+ });
+});
+
+describe('typeahead tests', function() {
+ it('should allow directives in template to require parent controller', function() {
+ module('ui.bootstrap.typeahead');
+ module('ngSanitize');
+ module('uib/template/typeahead/typeahead-popup.html');
+ module(function($compileProvider) {
+ $compileProvider
+ .directive('uibCustomParent', function() {
+ return {
+ controller: function() {
+ this.text = 'foo';
+ }
+ };
+ })
+ .directive('uibCustomDirective', function() {
+ return {
+ require: '^uibCustomParent',
+ link: function(scope, element, attrs, ctrl) {
+ scope.text = ctrl.text;
+ }
+ };
+ });
+ });
+
+ inject(function($compile, $rootScope, $sniffer, $templateCache) {
+ var element;
+ var $scope = $rootScope.$new();
+ $templateCache.put('uib/template/typeahead/typeahead-match.html', '
{{text}}
');
+ $scope.states = [
+ {code: 'AL', name: 'Alaska'},
+ {code: 'CL', name: 'California'}
+ ];
+
+ element = $compile('')($scope);
+ $rootScope.$digest();
+
+ var inputEl = element.find('input');
+ inputEl.val('Al');
+ inputEl.trigger($sniffer.hasEvent('input') ? 'input' : 'change');
+ $scope.$digest();
+
+ expect(element.find('ul.dropdown-menu li').eq(0).find('[uib-custom-directive]').text()).toEqual('foo');
+ });
+ });
});
diff --git a/src/typeahead/typeahead.css b/src/typeahead/typeahead.css
new file mode 100644
index 0000000000..8e2463fe4f
--- /dev/null
+++ b/src/typeahead/typeahead.css
@@ -0,0 +1,3 @@
+[uib-typeahead-popup].dropdown-menu {
+ display: block;
+}
diff --git a/src/typeahead/typeahead.js b/src/typeahead/typeahead.js
index 1fe28401ab..71b807c817 100644
--- a/src/typeahead/typeahead.js
+++ b/src/typeahead/typeahead.js
@@ -1,199 +1,524 @@
-angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap.bindHtml'])
+angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.debounce', 'ui.bootstrap.position'])
/**
* A helper service that can parse typeahead's syntax (string provided by users)
* Extracted to a separate service for ease of unit testing
*/
- .factory('typeaheadParser', ['$parse', function ($parse) {
-
- // 00000111000000000000022200000000000000003333333333333330000000000044000
- var TYPEAHEAD_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+([\s\S]+?)$/;
-
- return {
- parse:function (input) {
+ .factory('uibTypeaheadParser', ['$parse', function($parse) {
+ // 000001111111100000000000002222222200000000000000003333333333333330000000000044444444000
+ var TYPEAHEAD_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+([\s\S]+?)$/;
+ return {
+ parse: function(input) {
+ var match = input.match(TYPEAHEAD_REGEXP);
+ if (!match) {
+ throw new Error(
+ 'Expected typeahead specification in form of "_modelValue_ (as _label_)? for _item_ in _collection_"' +
+ ' but got "' + input + '".');
+ }
- var match = input.match(TYPEAHEAD_REGEXP);
- if (!match) {
- throw new Error(
- 'Expected typeahead specification in form of "_modelValue_ (as _label_)? for _item_ in _collection_"' +
- ' but got "' + input + '".');
+ return {
+ itemName: match[3],
+ source: $parse(match[4]),
+ viewMapper: $parse(match[2] || match[1]),
+ modelMapper: $parse(match[1])
+ };
}
+ };
+ }])
- return {
- itemName:match[3],
- source:$parse(match[4]),
- viewMapper:$parse(match[2] || match[1]),
- modelMapper:$parse(match[1])
- };
+ .controller('UibTypeaheadController', ['$scope', '$element', '$attrs', '$compile', '$parse', '$q', '$timeout', '$document', '$window', '$rootScope', '$$debounce', '$uibPosition', 'uibTypeaheadParser',
+ function(originalScope, element, attrs, $compile, $parse, $q, $timeout, $document, $window, $rootScope, $$debounce, $position, typeaheadParser) {
+ var HOT_KEYS = [9, 13, 27, 38, 40];
+ var eventDebounceTime = 200;
+ var modelCtrl, ngModelOptions;
+ //SUPPORTED ATTRIBUTES (OPTIONS)
+
+ //minimal no of characters that needs to be entered before typeahead kicks-in
+ var minLength = originalScope.$eval(attrs.typeaheadMinLength);
+ if (!minLength && minLength !== 0) {
+ minLength = 1;
}
- };
-}])
- .directive('typeahead', ['$compile', '$parse', '$q', '$timeout', '$document', '$position', 'typeaheadParser',
- function ($compile, $parse, $q, $timeout, $document, $position, typeaheadParser) {
+ originalScope.$watch(attrs.typeaheadMinLength, function (newVal) {
+ minLength = !newVal && newVal !== 0 ? 1 : newVal;
+ });
- var HOT_KEYS = [9, 13, 27, 38, 40];
+ //minimal wait time after last character typed before typeahead kicks-in
+ var waitTime = originalScope.$eval(attrs.typeaheadWaitMs) || 0;
- return {
- require:'ngModel',
- link:function (originalScope, element, attrs, modelCtrl) {
+ //should it restrict model values to the ones selected from the popup only?
+ var isEditable = originalScope.$eval(attrs.typeaheadEditable) !== false;
+ originalScope.$watch(attrs.typeaheadEditable, function (newVal) {
+ isEditable = newVal !== false;
+ });
- //SUPPORTED ATTRIBUTES (OPTIONS)
+ //binding to a variable that indicates if matches are being retrieved asynchronously
+ var isLoadingSetter = $parse(attrs.typeaheadLoading).assign || angular.noop;
- //minimal no of characters that needs to be entered before typeahead kicks-in
- var minSearch = originalScope.$eval(attrs.typeaheadMinLength) || 1;
+ //a function to determine if an event should cause selection
+ var isSelectEvent = attrs.typeaheadShouldSelect ? $parse(attrs.typeaheadShouldSelect) : function(scope, vals) {
+ var evt = vals.$event;
+ return evt.which === 13 || evt.which === 9;
+ };
- //minimal wait time after last character typed before typeahead kicks-in
- var waitTime = originalScope.$eval(attrs.typeaheadWaitMs) || 0;
+ //a callback executed when a match is selected
+ var onSelectCallback = $parse(attrs.typeaheadOnSelect);
- //should it restrict model values to the ones selected from the popup only?
- var isEditable = originalScope.$eval(attrs.typeaheadEditable) !== false;
+ //should it select highlighted popup value when losing focus?
+ var isSelectOnBlur = angular.isDefined(attrs.typeaheadSelectOnBlur) ? originalScope.$eval(attrs.typeaheadSelectOnBlur) : false;
- //binding to a variable that indicates if matches are being retrieved asynchronously
- var isLoadingSetter = $parse(attrs.typeaheadLoading).assign || angular.noop;
+ //binding to a variable that indicates if there were no results after the query is completed
+ var isNoResultsSetter = $parse(attrs.typeaheadNoResults).assign || angular.noop;
- //a callback executed when a match is selected
- var onSelectCallback = $parse(attrs.typeaheadOnSelect);
+ var inputFormatter = attrs.typeaheadInputFormatter ? $parse(attrs.typeaheadInputFormatter) : undefined;
- var inputFormatter = attrs.typeaheadInputFormatter ? $parse(attrs.typeaheadInputFormatter) : undefined;
+ var appendToBody = attrs.typeaheadAppendToBody ? originalScope.$eval(attrs.typeaheadAppendToBody) : false;
- var appendToBody = attrs.typeaheadAppendToBody ? originalScope.$eval(attrs.typeaheadAppendToBody) : false;
+ var appendTo = attrs.typeaheadAppendTo ?
+ originalScope.$eval(attrs.typeaheadAppendTo) : null;
- var focusFirst = originalScope.$eval(attrs.typeaheadFocusFirst) !== false;
+ var focusFirst = originalScope.$eval(attrs.typeaheadFocusFirst) !== false;
- //INTERNAL VARIABLES
+ //If input matches an item of the list exactly, select it automatically
+ var selectOnExact = attrs.typeaheadSelectOnExact ? originalScope.$eval(attrs.typeaheadSelectOnExact) : false;
- //model setter executed upon match selection
- var $setModelValue = $parse(attrs.ngModel).assign;
+ //binding to a variable that indicates if dropdown is open
+ var isOpenSetter = $parse(attrs.typeaheadIsOpen).assign || angular.noop;
- //expressions used by typeahead
- var parserResult = typeaheadParser.parse(attrs.typeahead);
+ var showHint = originalScope.$eval(attrs.typeaheadShowHint) || false;
- var hasFocus;
+ //INTERNAL VARIABLES
- //create a child scope for the typeahead directive so we are not polluting original scope
- //with typeahead-specific data (matches, query etc.)
- var scope = originalScope.$new();
- originalScope.$on('$destroy', function(){
- scope.$destroy();
- });
+ //model setter executed upon match selection
+ var parsedModel = $parse(attrs.ngModel);
+ var invokeModelSetter = $parse(attrs.ngModel + '($$$p)');
+ var $setModelValue = function(scope, newValue) {
+ if (angular.isFunction(parsedModel(originalScope)) &&
+ ngModelOptions.getOption('getterSetter')) {
+ return invokeModelSetter(scope, {$$$p: newValue});
+ }
- // WAI-ARIA
- var popupId = 'typeahead-' + scope.$id + '-' + Math.floor(Math.random() * 10000);
- element.attr({
- 'aria-autocomplete': 'list',
- 'aria-expanded': false,
- 'aria-owns': popupId
- });
+ return parsedModel.assign(scope, newValue);
+ };
- //pop-up element used to display matches
- var popUpEl = angular.element('');
- popUpEl.attr({
- id: popupId,
- matches: 'matches',
- active: 'activeIdx',
- select: 'select(activeIdx)',
- query: 'query',
- position: 'position'
+ //expressions used by typeahead
+ var parserResult = typeaheadParser.parse(attrs.uibTypeahead);
+
+ var hasFocus;
+
+ //Used to avoid bug in iOS webview where iOS keyboard does not fire
+ //mousedown & mouseup events
+ //Issue #3699
+ var selected;
+
+ //create a child scope for the typeahead directive so we are not polluting original scope
+ //with typeahead-specific data (matches, query etc.)
+ var scope = originalScope.$new();
+ var offDestroy = originalScope.$on('$destroy', function() {
+ scope.$destroy();
+ });
+ scope.$on('$destroy', offDestroy);
+
+ // WAI-ARIA
+ var popupId = 'typeahead-' + scope.$id + '-' + Math.floor(Math.random() * 10000);
+ element.attr({
+ 'aria-autocomplete': 'list',
+ 'aria-expanded': false,
+ 'aria-owns': popupId
+ });
+
+ var inputsContainer, hintInputElem;
+ //add read-only input to show hint
+ if (showHint) {
+ inputsContainer = angular.element('');
+ inputsContainer.css('position', 'relative');
+ element.after(inputsContainer);
+ hintInputElem = element.clone();
+ hintInputElem.attr('placeholder', '');
+ hintInputElem.attr('tabindex', '-1');
+ hintInputElem.val('');
+ hintInputElem.css({
+ 'position': 'absolute',
+ 'top': '0px',
+ 'left': '0px',
+ 'border-color': 'transparent',
+ 'box-shadow': 'none',
+ 'opacity': 1,
+ 'background': 'none 0% 0% / auto repeat scroll padding-box border-box rgb(255, 255, 255)',
+ 'color': '#999'
});
- //custom item template
- if (angular.isDefined(attrs.typeaheadTemplateUrl)) {
- popUpEl.attr('template-url', attrs.typeaheadTemplateUrl);
+ element.css({
+ 'position': 'relative',
+ 'vertical-align': 'top',
+ 'background-color': 'transparent'
+ });
+
+ if (hintInputElem.attr('id')) {
+ hintInputElem.removeAttr('id'); // remove duplicate id if present.
}
+ inputsContainer.append(hintInputElem);
+ hintInputElem.after(element);
+ }
- var resetMatches = function() {
- scope.matches = [];
- scope.activeIdx = -1;
- element.attr('aria-expanded', false);
- };
-
- var getMatchId = function(index) {
- return popupId + '-option-' + index;
- };
-
- // Indicate that the specified match is the active (pre-selected) item in the list owned by this typeahead.
- // This attribute is added or removed automatically when the `activeIdx` changes.
- scope.$watch('activeIdx', function(index) {
- if (index < 0) {
- element.removeAttr('aria-activedescendant');
- } else {
- element.attr('aria-activedescendant', getMatchId(index));
- }
- });
+ //pop-up element used to display matches
+ var popUpEl = angular.element('');
+ popUpEl.attr({
+ id: popupId,
+ matches: 'matches',
+ active: 'activeIdx',
+ select: 'select(activeIdx, evt)',
+ 'move-in-progress': 'moveInProgress',
+ query: 'query',
+ position: 'position',
+ 'assign-is-open': 'assignIsOpen(isOpen)',
+ debounce: 'debounceUpdate'
+ });
+ //custom item template
+ if (angular.isDefined(attrs.typeaheadTemplateUrl)) {
+ popUpEl.attr('template-url', attrs.typeaheadTemplateUrl);
+ }
- var getMatchesAsync = function(inputValue) {
-
- var locals = {$viewValue: inputValue};
- isLoadingSetter(originalScope, true);
- $q.when(parserResult.source(originalScope, locals)).then(function(matches) {
-
- //it might happen that several async queries were in progress if a user were typing fast
- //but we are interested only in responses that correspond to the current view value
- var onCurrentRequest = (inputValue === modelCtrl.$viewValue);
- if (onCurrentRequest && hasFocus) {
- if (matches && matches.length > 0) {
-
- scope.activeIdx = focusFirst ? 0 : -1;
- scope.matches.length = 0;
-
- //transform labels
- for(var i=0; i index && inputValue) {
+ return inputValue.toUpperCase() === scope.matches[index].label.toUpperCase();
+ }
- scope.query = inputValue;
- //position pop-up with matches - we need to re-calculate its position each time we are opening a window
- //with matches as a pop-up might be absolute-positioned and position of an input might have changed on a page
- //due to other elements being rendered
- scope.position = appendToBody ? $position.offset(element) : $position.position(element);
- scope.position.top = scope.position.top + element.prop('offsetHeight');
+ return false;
+ };
- element.attr('aria-expanded', true);
- } else {
- resetMatches();
+ var getMatchesAsync = function(inputValue, evt) {
+ var locals = {$viewValue: inputValue};
+ isLoadingSetter(originalScope, true);
+ isNoResultsSetter(originalScope, false);
+ $q.when(parserResult.source(originalScope, locals)).then(function(matches) {
+ //it might happen that several async queries were in progress if a user were typing fast
+ //but we are interested only in responses that correspond to the current view value
+ var onCurrentRequest = inputValue === modelCtrl.$viewValue;
+ if (onCurrentRequest && hasFocus) {
+ if (matches && matches.length > 0) {
+ scope.activeIdx = focusFirst ? 0 : -1;
+ isNoResultsSetter(originalScope, false);
+ scope.matches.length = 0;
+
+ //transform labels
+ for (var i = 0; i < matches.length; i++) {
+ locals[parserResult.itemName] = matches[i];
+ scope.matches.push({
+ id: getMatchId(i),
+ label: parserResult.viewMapper(scope, locals),
+ model: matches[i]
+ });
}
+
+ scope.query = inputValue;
+ //position pop-up with matches - we need to re-calculate its position each time we are opening a window
+ //with matches as a pop-up might be absolute-positioned and position of an input might have changed on a page
+ //due to other elements being rendered
+ recalculatePosition();
+
+ element.attr('aria-expanded', true);
+
+ //Select the single remaining option if user input matches
+ if (selectOnExact && scope.matches.length === 1 && inputIsExactMatch(inputValue, 0)) {
+ if (angular.isNumber(scope.debounceUpdate) || angular.isObject(scope.debounceUpdate)) {
+ $$debounce(function() {
+ scope.select(0, evt);
+ }, angular.isNumber(scope.debounceUpdate) ? scope.debounceUpdate : scope.debounceUpdate['default']);
+ } else {
+ scope.select(0, evt);
+ }
+ }
+
+ if (showHint) {
+ var firstLabel = scope.matches[0].label;
+ if (angular.isString(inputValue) &&
+ inputValue.length > 0 &&
+ firstLabel.slice(0, inputValue.length).toUpperCase() === inputValue.toUpperCase()) {
+ hintInputElem.val(inputValue + firstLabel.slice(inputValue.length));
+ } else {
+ hintInputElem.val('');
+ }
+ }
+ } else {
+ resetMatches();
+ isNoResultsSetter(originalScope, true);
}
- if (onCurrentRequest) {
- isLoadingSetter(originalScope, false);
- }
- }, function(){
- resetMatches();
+ }
+ if (onCurrentRequest) {
isLoadingSetter(originalScope, false);
- });
- };
+ }
+ }, function() {
+ resetMatches();
+ isLoadingSetter(originalScope, false);
+ isNoResultsSetter(originalScope, true);
+ });
+ };
+
+ // bind events only if appendToBody params exist - performance feature
+ if (appendToBody) {
+ angular.element($window).on('resize', fireRecalculating);
+ $document.find('body').on('scroll', fireRecalculating);
+ }
+
+ // Declare the debounced function outside recalculating for
+ // proper debouncing
+ var debouncedRecalculate = $$debounce(function() {
+ // if popup is visible
+ if (scope.matches.length) {
+ recalculatePosition();
+ }
+
+ scope.moveInProgress = false;
+ }, eventDebounceTime);
+
+ // Default progress type
+ scope.moveInProgress = false;
+
+ function fireRecalculating() {
+ if (!scope.moveInProgress) {
+ scope.moveInProgress = true;
+ scope.$digest();
+ }
+
+ debouncedRecalculate();
+ }
+
+ // recalculate actual position and set new values to scope
+ // after digest loop is popup in right position
+ function recalculatePosition() {
+ scope.position = appendToBody ? $position.offset(element) : $position.position(element);
+ scope.position.top += element.prop('offsetHeight');
+ }
+
+ //we need to propagate user's query so we can higlight matches
+ scope.query = undefined;
+
+ //Declare the timeout promise var outside the function scope so that stacked calls can be cancelled later
+ var timeoutPromise;
+
+ var scheduleSearchWithTimeout = function(inputValue) {
+ timeoutPromise = $timeout(function() {
+ getMatchesAsync(inputValue);
+ }, waitTime);
+ };
+
+ var cancelPreviousTimeout = function() {
+ if (timeoutPromise) {
+ $timeout.cancel(timeoutPromise);
+ }
+ };
+
+ resetMatches();
+
+ scope.assignIsOpen = function (isOpen) {
+ isOpenSetter(originalScope, isOpen);
+ };
+
+ scope.select = function(activeIdx, evt) {
+ //called from within the $digest() cycle
+ var locals = {};
+ var model, item;
+
+ selected = true;
+ locals[parserResult.itemName] = item = scope.matches[activeIdx].model;
+ model = parserResult.modelMapper(originalScope, locals);
+ $setModelValue(originalScope, model);
+ modelCtrl.$setValidity('editable', true);
+ modelCtrl.$setValidity('parse', true);
+
+ onSelectCallback(originalScope, {
+ $item: item,
+ $model: model,
+ $label: parserResult.viewMapper(originalScope, locals),
+ $event: evt
+ });
resetMatches();
- //we need to propagate user's query so we can higlight matches
- scope.query = undefined;
+ //return focus to the input element if a match was selected via a mouse click event
+ // use timeout to avoid $rootScope:inprog error
+ if (scope.$eval(attrs.typeaheadFocusOnSelect) !== false) {
+ $timeout(function() { element[0].focus(); }, 0, false);
+ }
+ };
+
+ //bind keyboard events: arrows up(38) / down(40), enter(13) and tab(9), esc(27)
+ element.on('keydown', function(evt) {
+ //typeahead is open and an "interesting" key was pressed
+ if (scope.matches.length === 0 || HOT_KEYS.indexOf(evt.which) === -1) {
+ return;
+ }
- //Declare the timeout promise var outside the function scope so that stacked calls can be cancelled later
- var timeoutPromise;
+ var shouldSelect = isSelectEvent(originalScope, {$event: evt});
+
+ /**
+ * if there's nothing selected (i.e. focusFirst) and enter or tab is hit
+ * or
+ * shift + tab is pressed to bring focus to the previous element
+ * then clear the results
+ */
+ if (scope.activeIdx === -1 && shouldSelect || evt.which === 9 && !!evt.shiftKey) {
+ resetMatches();
+ scope.$digest();
+ return;
+ }
- var scheduleSearchWithTimeout = function(inputValue) {
- timeoutPromise = $timeout(function () {
- getMatchesAsync(inputValue);
- }, waitTime);
- };
+ evt.preventDefault();
+ var target;
+ switch (evt.which) {
+ case 27: // escape
+ evt.stopPropagation();
- var cancelPreviousTimeout = function() {
- if (timeoutPromise) {
- $timeout.cancel(timeoutPromise);
+ resetMatches();
+ originalScope.$digest();
+ break;
+ case 38: // up arrow
+ scope.activeIdx = (scope.activeIdx > 0 ? scope.activeIdx : scope.matches.length) - 1;
+ scope.$digest();
+ target = popUpEl[0].querySelectorAll('.uib-typeahead-match')[scope.activeIdx];
+ target.parentNode.scrollTop = target.offsetTop;
+ break;
+ case 40: // down arrow
+ scope.activeIdx = (scope.activeIdx + 1) % scope.matches.length;
+ scope.$digest();
+ target = popUpEl[0].querySelectorAll('.uib-typeahead-match')[scope.activeIdx];
+ target.parentNode.scrollTop = target.offsetTop;
+ break;
+ default:
+ if (shouldSelect) {
+ scope.$apply(function() {
+ if (angular.isNumber(scope.debounceUpdate) || angular.isObject(scope.debounceUpdate)) {
+ $$debounce(function() {
+ scope.select(scope.activeIdx, evt);
+ }, angular.isNumber(scope.debounceUpdate) ? scope.debounceUpdate : scope.debounceUpdate['default']);
+ } else {
+ scope.select(scope.activeIdx, evt);
+ }
+ });
+ }
+ }
+ });
+
+ element.on('focus', function (evt) {
+ hasFocus = true;
+ if (minLength === 0 && !modelCtrl.$viewValue) {
+ $timeout(function() {
+ getMatchesAsync(modelCtrl.$viewValue, evt);
+ }, 0);
+ }
+ });
+
+ element.on('blur', function(evt) {
+ if (isSelectOnBlur && scope.matches.length && scope.activeIdx !== -1 && !selected) {
+ selected = true;
+ scope.$apply(function() {
+ if (angular.isObject(scope.debounceUpdate) && angular.isNumber(scope.debounceUpdate.blur)) {
+ $$debounce(function() {
+ scope.select(scope.activeIdx, evt);
+ }, scope.debounceUpdate.blur);
+ } else {
+ scope.select(scope.activeIdx, evt);
+ }
+ });
+ }
+ if (!isEditable && modelCtrl.$error.editable) {
+ modelCtrl.$setViewValue();
+ scope.$apply(function() {
+ // Reset validity as we are clearing
+ modelCtrl.$setValidity('editable', true);
+ modelCtrl.$setValidity('parse', true);
+ });
+ element.val('');
+ }
+ hasFocus = false;
+ selected = false;
+ });
+
+ // Keep reference to click handler to unbind it.
+ var dismissClickHandler = function(evt) {
+ // Issue #3973
+ // Firefox treats right click as a click on document
+ if (element[0] !== evt.target && evt.which !== 3 && scope.matches.length !== 0) {
+ resetMatches();
+ if (!$rootScope.$$phase) {
+ originalScope.$digest();
}
- };
+ }
+ };
+
+ $document.on('click', dismissClickHandler);
+
+ originalScope.$on('$destroy', function() {
+ $document.off('click', dismissClickHandler);
+ if (appendToBody || appendTo) {
+ $popup.remove();
+ }
+
+ if (appendToBody) {
+ angular.element($window).off('resize', fireRecalculating);
+ $document.find('body').off('scroll', fireRecalculating);
+ }
+ // Prevent jQuery cache memory leak
+ popUpEl.remove();
+
+ if (showHint) {
+ inputsContainer.remove();
+ }
+ });
+
+ var $popup = $compile(popUpEl)(scope);
+
+ if (appendToBody) {
+ $document.find('body').append($popup);
+ } else if (appendTo) {
+ angular.element(appendTo).eq(0).append($popup);
+ } else {
+ element.after($popup);
+ }
+
+ this.init = function(_modelCtrl) {
+ modelCtrl = _modelCtrl;
+ ngModelOptions = extractOptions(modelCtrl);
+
+ scope.debounceUpdate = $parse(ngModelOptions.getOption('debounce'))(originalScope);
//plug into $parsers pipeline to open a typeahead on view changes initiated from DOM
//$parsers kick-in on all the changes coming from the view as well as manually triggered by $setViewValue
- modelCtrl.$parsers.unshift(function (inputValue) {
-
+ modelCtrl.$parsers.unshift(function(inputValue) {
hasFocus = true;
- if (inputValue && inputValue.length >= minSearch) {
+ if (minLength === 0 || inputValue && inputValue.length >= minLength) {
if (waitTime > 0) {
cancelPreviousTimeout();
scheduleSearchWithTimeout(inputValue);
@@ -208,20 +533,19 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap
if (isEditable) {
return inputValue;
- } else {
- if (!inputValue) {
- // Reset in case user had typed something previously.
- modelCtrl.$setValidity('editable', true);
- return inputValue;
- } else {
- modelCtrl.$setValidity('editable', false);
- return undefined;
- }
}
- });
- modelCtrl.$formatters.push(function (modelValue) {
+ if (!inputValue) {
+ // Reset in case user had typed something previously.
+ modelCtrl.$setValidity('editable', true);
+ return null;
+ }
+
+ modelCtrl.$setValidity('editable', false);
+ return undefined;
+ });
+ modelCtrl.$formatters.push(function(modelValue) {
var candidateViewValue, emptyViewValue;
var locals = {};
@@ -233,179 +557,137 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap
}
if (inputFormatter) {
-
locals.$model = modelValue;
return inputFormatter(originalScope, locals);
-
- } else {
-
- //it might happen that we don't have enough info to properly render input value
- //we need to check for this situation and simply return model value if we can't apply custom formatting
- locals[parserResult.itemName] = modelValue;
- candidateViewValue = parserResult.viewMapper(originalScope, locals);
- locals[parserResult.itemName] = undefined;
- emptyViewValue = parserResult.viewMapper(originalScope, locals);
-
- return candidateViewValue!== emptyViewValue ? candidateViewValue : modelValue;
}
- });
- scope.select = function (activeIdx) {
- //called from within the $digest() cycle
- var locals = {};
- var model, item;
-
- locals[parserResult.itemName] = item = scope.matches[activeIdx].model;
- model = parserResult.modelMapper(originalScope, locals);
- $setModelValue(originalScope, model);
- modelCtrl.$setValidity('editable', true);
- modelCtrl.$setValidity('parse', true);
-
- onSelectCallback(originalScope, {
- $item: item,
- $model: model,
- $label: parserResult.viewMapper(originalScope, locals)
- });
+ //it might happen that we don't have enough info to properly render input value
+ //we need to check for this situation and simply return model value if we can't apply custom formatting
+ locals[parserResult.itemName] = modelValue;
+ candidateViewValue = parserResult.viewMapper(originalScope, locals);
+ locals[parserResult.itemName] = undefined;
+ emptyViewValue = parserResult.viewMapper(originalScope, locals);
- resetMatches();
-
- //return focus to the input element if a match was selected via a mouse click event
- // use timeout to avoid $rootScope:inprog error
- $timeout(function() { element[0].focus(); }, 0, false);
- };
-
- //bind keyboard events: arrows up(38) / down(40), enter(13) and tab(9), esc(27)
- element.bind('keydown', function (evt) {
-
- //typeahead is open and an "interesting" key was pressed
- if (scope.matches.length === 0 || HOT_KEYS.indexOf(evt.which) === -1) {
- return;
- }
-
- // if there's nothing selected (i.e. focusFirst) and enter is hit, don't do anything
- if (scope.activeIdx == -1 && (evt.which === 13 || evt.which === 9)) {
- return;
- }
-
- evt.preventDefault();
-
- if (evt.which === 40) {
- scope.activeIdx = (scope.activeIdx + 1) % scope.matches.length;
- scope.$digest();
-
- } else if (evt.which === 38) {
- scope.activeIdx = (scope.activeIdx > 0 ? scope.activeIdx : scope.matches.length) - 1;
- scope.$digest();
-
- } else if (evt.which === 13 || evt.which === 9) {
- scope.$apply(function () {
- scope.select(scope.activeIdx);
- });
-
- } else if (evt.which === 27) {
- evt.stopPropagation();
-
- resetMatches();
- scope.$digest();
- }
- });
-
- element.bind('blur', function (evt) {
- hasFocus = false;
+ return candidateViewValue !== emptyViewValue ? candidateViewValue : modelValue;
});
+ };
- // Keep reference to click handler to unbind it.
- var dismissClickHandler = function (evt) {
- if (element[0] !== evt.target) {
- resetMatches();
- scope.$digest();
- }
- };
-
- $document.bind('click', dismissClickHandler);
-
- originalScope.$on('$destroy', function(){
- $document.unbind('click', dismissClickHandler);
- if (appendToBody) {
- $popup.remove();
- }
- // Prevent jQuery cache memory leak
- popUpEl.remove();
- });
+ function extractOptions(ngModelCtrl) {
+ var ngModelOptions;
- var $popup = $compile(popUpEl)(scope);
+ if (angular.version.minor < 6) { // in angular < 1.6 $options could be missing
+ // guarantee a value
+ ngModelOptions = ngModelCtrl.$options || {};
- if (appendToBody) {
- $document.find('body').append($popup);
- } else {
- element.after($popup);
+ // mimic 1.6+ api
+ ngModelOptions.getOption = function (key) {
+ return ngModelOptions[key];
+ };
+ } else { // in angular >=1.6 $options is always present
+ ngModelOptions = ngModelCtrl.$options;
}
+
+ return ngModelOptions;
}
- };
+ }])
-}])
+ .directive('uibTypeahead', function() {
+ return {
+ controller: 'UibTypeaheadController',
+ require: ['ngModel', 'uibTypeahead'],
+ link: function(originalScope, element, attrs, ctrls) {
+ ctrls[1].init(ctrls[0]);
+ }
+ };
+ })
- .directive('typeaheadPopup', function () {
+ .directive('uibTypeaheadPopup', ['$$debounce', function($$debounce) {
return {
- restrict:'EA',
- scope:{
- matches:'=',
- query:'=',
- active:'=',
- position:'=',
- select:'&'
+ scope: {
+ matches: '=',
+ query: '=',
+ active: '=',
+ position: '&',
+ moveInProgress: '=',
+ select: '&',
+ assignIsOpen: '&',
+ debounce: '&'
},
- replace:true,
- templateUrl:'template/typeahead/typeahead-popup.html',
- link:function (scope, element, attrs) {
-
+ replace: true,
+ templateUrl: function(element, attrs) {
+ return attrs.popupTemplateUrl || 'uib/template/typeahead/typeahead-popup.html';
+ },
+ link: function(scope, element, attrs) {
scope.templateUrl = attrs.templateUrl;
- scope.isOpen = function () {
- return scope.matches.length > 0;
+ scope.isOpen = function() {
+ var isDropdownOpen = scope.matches.length > 0;
+ scope.assignIsOpen({ isOpen: isDropdownOpen });
+ return isDropdownOpen;
};
- scope.isActive = function (matchIdx) {
- return scope.active == matchIdx;
+ scope.isActive = function(matchIdx) {
+ return scope.active === matchIdx;
};
- scope.selectActive = function (matchIdx) {
+ scope.selectActive = function(matchIdx) {
scope.active = matchIdx;
};
- scope.selectMatch = function (activeIdx) {
- scope.select({activeIdx:activeIdx});
+ scope.selectMatch = function(activeIdx, evt) {
+ var debounce = scope.debounce();
+ if (angular.isNumber(debounce) || angular.isObject(debounce)) {
+ $$debounce(function() {
+ scope.select({activeIdx: activeIdx, evt: evt});
+ }, angular.isNumber(debounce) ? debounce : debounce['default']);
+ } else {
+ scope.select({activeIdx: activeIdx, evt: evt});
+ }
};
}
};
- })
+ }])
- .directive('typeaheadMatch', ['$templateRequest', '$compile', '$parse', function ($templateRequest, $compile, $parse) {
+ .directive('uibTypeaheadMatch', ['$templateRequest', '$compile', '$parse', function($templateRequest, $compile, $parse) {
return {
- restrict:'EA',
- scope:{
- index:'=',
- match:'=',
- query:'='
+ scope: {
+ index: '=',
+ match: '=',
+ query: '='
},
- link:function (scope, element, attrs) {
- var tplUrl = $parse(attrs.templateUrl)(scope.$parent) || 'template/typeahead/typeahead-match.html';
+ link: function(scope, element, attrs) {
+ var tplUrl = $parse(attrs.templateUrl)(scope.$parent) || 'uib/template/typeahead/typeahead-match.html';
$templateRequest(tplUrl).then(function(tplContent) {
- $compile(tplContent.trim())(scope, function(clonedElement){
- element.replaceWith(clonedElement);
- });
+ var tplEl = angular.element(tplContent.trim());
+ element.replaceWith(tplEl);
+ $compile(tplEl)(scope);
});
}
};
}])
- .filter('typeaheadHighlight', function() {
+ .filter('uibTypeaheadHighlight', ['$sce', '$injector', '$log', function($sce, $injector, $log) {
+ var isSanitizePresent;
+ isSanitizePresent = $injector.has('$sanitize');
function escapeRegexp(queryToEscape) {
+ // Regex: capture the whole query string and replace it with the string that will be used to match
+ // the results, for example if the capture is "a" the result will be \a
return queryToEscape.replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1');
}
+ function containsHtml(matchItem) {
+ return /<.*>/g.test(matchItem);
+ }
+
return function(matchItem, query) {
- return query ? ('' + matchItem).replace(new RegExp(escapeRegexp(query), 'gi'), '$&') : matchItem;
+ if (!isSanitizePresent && containsHtml(matchItem)) {
+ $log.warn('Unsafe use of typeahead please use ngSanitize'); // Warn the user about the danger
+ }
+ matchItem = query ? ('' + matchItem).replace(new RegExp(escapeRegexp(query), 'gi'), '$&') : matchItem; // Replaces the capture string with a the same string inside of a "strong" tag
+ if (!isSanitizePresent) {
+ matchItem = $sce.trustAsHtml(matchItem); // If $sanitize is not present we pack the string in a $sce object for the ng-bind-html directive
+ }
+ return matchItem;
};
- });
+ }]);
diff --git a/template/accordion/accordion-group.html b/template/accordion/accordion-group.html
index d220401a63..8cae8c023b 100644
--- a/template/accordion/accordion-group.html
+++ b/template/accordion/accordion-group.html
@@ -1,10 +1,8 @@
-
diff --git a/template/accordion/accordion.html b/template/accordion/accordion.html
index ba428f3b5e..2a55b9bd73 100644
--- a/template/accordion/accordion.html
+++ b/template/accordion/accordion.html
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/template/alert/alert.html b/template/alert/alert.html
index c813624858..b5bade4b0d 100644
--- a/template/alert/alert.html
+++ b/template/alert/alert.html
@@ -1,7 +1,5 @@
-
\ No newline at end of file
diff --git a/template/pagination/pagination.html b/template/pagination/pagination.html
index acd101a995..cf52a94e8d 100644
--- a/template/pagination/pagination.html
+++ b/template/pagination/pagination.html
@@ -1,7 +1,5 @@
-